diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b690b100..b296f2ddf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,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] @@ -663,11 +665,9 @@ jobs: if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }} name: test (macOS arm64 LLVM artifacts, no clang, ${{ matrix.shard }}/5) runs-on: macos-15 - # 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. Slow runners need more - # headroom than that, so the limit is 50 minutes. + # Each LLVM differential slice takes 30 to 35 minutes on a hosted runner. + # The package/install and link-parity contracts run in their own job + # (llvm_artifacts_macos_arm64_contracts), so no slice carries them. timeout-minutes: 50 strategy: fail-fast: false @@ -678,6 +678,36 @@ jobs: # keeping each macOS job below its timeout while the matrix union still # exercises every LLVM-tier program. SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/5 + 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-darwin-arm64 + path: ${{ runner.temp }} + - run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz" + - run: pnpm build + - name: LLVM release/dev differential (${{ matrix.shard }}/5) + env: + SCRIPTC_TEST_WORKERS: "4" + run: pnpm test tests/harness/llvm-differential.test.ts + + llvm_artifacts_macos_arm64_contracts: + needs: [validation_scope, lanes, native_macos] + if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }} + name: test (macOS arm64 LLVM artifacts, no clang, contracts) + runs-on: macos-15 + # The no-clang object, helper parity, packaged helper and packed npm + # installation contracts: about 12 minutes, which used to run in front + # of shard 1's differential slice and took that job to 45-50 minutes. + timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -695,10 +725,8 @@ jobs: - run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz" - run: pnpm build - name: No-clang assembly/object contract - if: matrix.shard == 1 run: pnpm test packages/cli/test/native-output.test.ts - name: Helper diagnostics and object/link parity - if: matrix.shard == 1 run: >- pnpm test packages/compiler/test/native-codegen-integration.test.ts @@ -706,17 +734,11 @@ jobs: packages/cli/test/native-link-info.test.ts packages/cli/test/runtime-pack.test.ts tests/harness/native-object-example.test.ts - - name: LLVM release/dev differential (${{ matrix.shard }}/5) - env: - SCRIPTC_TEST_WORKERS: "4" - run: pnpm test tests/harness/llvm-differential.test.ts - name: Packaged helper contract - if: matrix.shard == 1 run: | TARBALL=$(pnpm --dir packages/llvm-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent) node scripts/verify-llvm-package.mjs "$RUNNER_TEMP/$(basename "$TARBALL")" - name: Packed npm installation smoke - if: matrix.shard == 1 env: # Release optimization is covered by the compiler bootstrap jobs. # This lane verifies packaging and installation with a faster seed. @@ -989,6 +1011,7 @@ jobs: checker_identity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, + llvm_artifacts_macos_arm64_contracts, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64, @@ -1022,6 +1045,7 @@ jobs: lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}" + lane llvm_artifacts_macos_arm64_contracts "${{ needs.llvm_artifacts_macos_arm64_contracts.result }}" "${{ needs.lanes.outputs.macos }}" lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}" lane windows_cli "${{ needs.windows_cli.result }}" "${{ needs.lanes.outputs.linux }}" lane llvm_artifacts_windows_x64 "${{ needs.llvm_artifacts_windows_x64.result }}" "${{ needs.lanes.outputs.linux }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 052811da9..a402f912f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,17 +101,17 @@ jobs: if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`); NODE - # Collects the native compiler's profile on this commit - # (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes - # with a profile of the code it compiles. The committed profile falls behind - # main within a day or two. If this job fails, the CLI builds use the - # committed profile. + # Collects the native compiler's function layout and profile on this commit + # (scripts/refresh-cli-layout.mjs, scripts/refresh-cli-profile.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 + timeout-minutes: 75 continue-on-error: true permissions: contents: read @@ -136,14 +136,26 @@ jobs: 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" + # The layout comes first: the profile matches the functions of builds + # with the layout its instrumented compiler was built with, so the + # profile is collected with a second instrumented compiler built with + # this layout. + - name: Collect the function layout + run: >- + node scripts/refresh-cli-layout.mjs --jobs 4 + --output "$RUNNER_TEMP/cli-profile/cli.layout.gz" --work "$RUNNER_TEMP/cli-layout-work" - name: Collect the profile + env: + SCRIPTC_NATIVE_LAYOUT: ${{ runner.temp }}/cli-profile/cli.layout.gz run: >- node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4 - --output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" + --output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" --work "$RUNNER_TEMP/cli-profile-work" - uses: actions/upload-artifact@v4 with: name: cli-profile - path: ${{ runner.temp }}/cli-profile/cli.profdata.gz + path: | + ${{ runner.temp }}/cli-profile/cli.profdata.gz + ${{ runner.temp }}/cli-profile/cli.layout.gz retention-days: 7 if-no-files-found: error @@ -336,8 +348,8 @@ jobs: with: name: cli-profile path: ${{ runner.temp }}/cli-profile - # Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and - # build-native-cli.mts uses the committed profile. + # Without the artifact, SCRIPTC_NATIVE_PROFILE_USE and SCRIPTC_NATIVE_LAYOUT + # stay unset and build-native-cli.mts uses the committed profile and layout. - name: Select the native compiler profile if: matrix.cli != '' shell: bash @@ -347,6 +359,11 @@ jobs: if [ -n "$COLLECTED" ]; then echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV" echo "The native CLI builds with the profile collected on $GITHUB_SHA" + layout="$(dirname "$COLLECTED")/cli.layout.gz" + if [ -f "$layout" ]; then + echo "SCRIPTC_NATIVE_LAYOUT=$layout" >> "$GITHUB_ENV" + echo "The native CLI builds with the function layout collected on $GITHUB_SHA" + fi else echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile" fi @@ -424,7 +441,8 @@ 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 @@ -577,9 +595,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; the npm-available - # job waits for it. + # 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 @@ -599,11 +616,24 @@ 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/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" 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 55ccb2eab..3e631a92f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,8 @@ their supported native-clang contracts locally; other hosts retain those checks in the Sandboxes. Both lanes green is the bar before shipping any change. +`SCRIPTC_SANDBOX_NATIVE_REFERENCE` names a built checkout inside a prepared `SCRIPTC_SANDBOX_SNAPSHOT` or image: each lane copies that checkout's LLVM helper and runtime pack instead of rebuilding them when their source inputs are identical. Sandbox creation is rate limited per team in vCPUs per minute, so the runner retries a rate-limited creation with backoff. Larger Sandboxes (`SCRIPTC_SANDBOX_VCPUS` up to 32, with `SCRIPTC_TEST_WORKERS` at half the vCPUs) shorten the lanes most. + `VERCEL_OIDC_TOKEN` is the preferred credential. `VERCEL_TOKEN` remains supported with explicit `VERCEL_TEAM_ID` and `VERCEL_PROJECT_ID`. The custom `SCRIPTC_SANDBOX_IMAGE` is optional: without it, the gate starts from @@ -58,6 +60,8 @@ A change that must not cost good code anything, such as a compatibility fix that node scripts/zero-cost-gate.mjs --baseline=/vercel/sandbox/prev --candidate=. --allow='\barguments\b' ``` +The whole gate takes 30 to 75 minutes on one 8-vCPU Sandbox, so shard it across Sandboxes. `--shard=i/n` splits the bench and corpus programs; the self-hosted compiler runs in shard 1, or alone with `--set=self`, and its candidate-source build overlaps its comparison build on hosts with 40 GB or more. Each shard writes `--json`, and `node scripts/zero-cost-merge.mjs ...` merges them into one verdict, recomputing the compile-time intervals over every shard. `--record-baseline-cache=` builds only the baseline and records each program's artifact hashes; `--baseline-cache=` then skips the baseline build of every program whose candidate artifacts hash the same, which halves the work for a change that leaves most programs identical. A record applies only to the built baseline checkout, toolchain, and paths it was made with, and is ignored otherwise; a program whose entry, relative imports, or optimization level changed since the record builds its baseline. + Test location follows scope: - Co-locate white-box unit tests with implementation files under @@ -68,6 +72,16 @@ Test location follows scope: Keep existing tests in place unless a change already touches their organization; new tests should follow this convention. +## Numbered registries + +Parallel changes must never pick the same number by hand. Each registry has one source of truth and a fast check: + +- Diagnostic codes: claim a code by adding its row, in numeric order, to `DIAGNOSTIC_CODES` in `packages/compiler/src/diagnostics/codes.ts` before any site uses it. A shipped construct's code moves to `RETIRED_DIAGNOSTIC_CODES` and is never reused; codes that open work already uses are held in `RESERVED_DIAGNOSTIC_CODES`. `codes.test.ts` fails on a code without a row, a retired code in use, an unused row, or rows out of order, and two claims of one code are a merge conflict or a duplicate key. +- Dyn handle tags: add a new tag by name only to `SCR_DYNH_NEW_TAGS` in `packages/runtime/src/scr_dyn.h`, in name order; the enum numbers it. Pin a tag to an explicit value in `SCR_DYNH_TAGS` only when the LLVM backend needs its number. The header does not compile when two tags share a value, and `dyn-handle-tags.test.ts` names the problem. +- Runtime enums and flag bits the backend mirrors (`ScrDynKind`, `ScrMessageKind`, `ScrBytesElem`, `SCR_CLOSURE_KIND_*`, the `%ScrArr` spare flags) are checked against their C declarations by `packages/compiler/src/backend/llvm/runtime-registries.test.ts`. A new mirrored enum or flag set gets a check there, and C code names flag bits instead of writing their values. + +Before taking a number in a pinned registry, run `node scripts/registry-claims.mjs`: it lists the diagnostic codes and pinned handle tags that open pull requests claim and reports collisions; `--check` reports only collisions with the current branch and exits 1 when there is one. + ## Where things live - `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM backend. diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index 61f5d8ad9..7a943a85f 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -26,6 +26,7 @@ RUN apt-get update \ libclang-rt-22-dev \ libpolly-22-dev \ libzstd-dev \ + lld-22 \ llvm-22-dev \ ninja-build \ xz-utils \ @@ -33,6 +34,7 @@ RUN apt-get update \ && ln -sf clang-22 /usr/bin/clang \ && ln -sf clang++-22 /usr/bin/clang++ \ && test "$(llvm-config-22 --version)" = 22.1.8 \ + && test -x "$(dirname "$(readlink -f /usr/bin/clang)")/ld.lld" \ && rm -rf /var/lib/apt/lists/* RUN curl --fail --silent --show-error --location \ diff --git a/RELEASING.md b/RELEASING.md index 42bfe75ad..a33da97f5 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 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. +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 profile. The profile keeps the functions that training never ran, with zero counts, so the CLI builds optimize them as cold code and place them after the code that ran. 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`), and the CLI builds use it (`SCRIPTC_NATIVE_LAYOUT`). Instrumented builds use the same layout as optimized builds, because a function laid out in a different partition than in the instrumented build no longer matches its profile record. The job therefore collects the layout first and then builds the instrumented compiler for the profile with that layout. Refresh the committed layout the same way, with `node scripts/refresh-cli-layout.mjs`, before the profile, and commit each 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 9af4ace00..f3a444ea9 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -28,6 +28,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | | `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | | `published-graphs` | cpu | syntax trees with parent links and a symbol table, published with `@scriptc/threads` and then read | +| `record-untyped-writes` | cpu | typed records written through untyped views: a counter helper taking `unknown` bumps fields of a stats record, and `Object.assign` merges patches into a settings record, with typed reads in between | | `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` | diff --git a/benchmarks/runtime/workloads/record-untyped-writes.json b/benchmarks/runtime/workloads/record-untyped-writes.json new file mode 100644 index 000000000..35f8e53fc --- /dev/null +++ b/benchmarks/runtime/workloads/record-untyped-writes.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/record-untyped-writes.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/record-untyped-writes.ts b/benchmarks/runtime/workloads/record-untyped-writes.ts new file mode 100644 index 000000000..956f7955b --- /dev/null +++ b/benchmarks/runtime/workloads/record-untyped-writes.ts @@ -0,0 +1,49 @@ +// Typed records updated through untyped views: a stats record written by a +// generic counter helper that takes `unknown`, settings merged with +// Object.assign, and typed reads in between. Each write stores into the +// native record; typed code reads the same object. +type Stats = { hits: number; misses: number; bytes: number; name: string; ok: boolean }; +type Settings = { host: string; port: number; debug: boolean; retries: number }; + +const scale = Number(process.argv[2] ?? "1"); + +function bump(target: unknown, key: string, by: number): void { + const record = target as Record; + record[key] = record[key]! + by; +} + +const stats: Stats[] = []; +for (let i = 0; i < 16; i++) stats.push({ hits: 0, misses: 0, bytes: 0, name: "s" + i, ok: true }); + +let seed = 12345; +let checksum = 0; +const rounds = Math.floor(1_000_000 * scale); +for (let i = 0; i < rounds; i++) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + const s = stats[seed & 15]!; + if ((seed >>> 8) % 5 === 0) bump(s, "misses", 1); + else bump(s, "hits", 1); + bump(s, "bytes", (seed >>> 12) & 1023); + checksum += s.hits - s.misses; +} + +const settings: Settings = { host: "localhost", port: 80, debug: false, retries: 0 }; +const view: object = settings; +const patches: object[] = [ + { port: 8080 }, + { debug: true, retries: 3 }, + { host: "example.test" }, + { port: 443, debug: false }, +]; +for (let i = 0; i < Math.floor(200_000 * scale); i++) { + Object.assign(view, patches[i & 3]!); + checksum += settings.port + settings.retries; +} + +let hits = 0; +let bytes = 0; +for (const s of stats) { + hits += s.hits; + bytes += s.bytes; +} +console.log("hits", hits, "bytes", bytes, "settings", JSON.stringify(settings), "checksum", checksum); diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 5afa6ff73..b1583a617 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -139,8 +139,8 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--print <native-link-info|diagnostics>
native-link-info: build an object (equivalent to --emit=obj) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker. diagnostics: for build and coverage, print the versioned JSON diagnostics document described in Coverage Reports instead of the human output.
-
--fail-on <blockers|divergences|dynamic>
-
Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with divergences, also when it has Node divergence warnings, or with dynamic, also when it has the SC6101 warning for untyped code that likely runs often. See Gating builds and CI.
+
--fail-on <blockers|divergences|dynamic|dynamic-slots>
+
Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with divergences, also when it has Node divergence warnings, with dynamic, also when it has the SC6101 warning for untyped code that likely runs often, or with dynamic-slots, also when it has dynamic slots (SC6104). See Gating builds and CI.
--ffi <file>
Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See Native FFI.
@@ -151,6 +151,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--strip
Remove symbol and debug payload when linking an executable, including cross-compiled targets. This flag preserves code optimization.
+
--static
+
On Linux, link the C library and the other system libraries into the executable. The executable starts without the dynamic loader, which shortens its startup, and doesn't depend on the run host's glibc version. glibc executables link as static PIE, are about 1 MB larger, and need glibc's static libraries on the build machine (libc6-dev on Debian and Ubuntu, glibc-static on Fedora); they link only for the build machine's own glibc target. A program whose executable includes host, user, or group name lookups, which dns.lookup, net and http connections, fetch, and os.userInfo() use, is refused with SC3002: a static glibc executable loads the NSS modules for those lookups from the run host, which glibc supports only for the glibc version it was linked with. Build such a program without --static, or for an <arch>-linux-musl target. musl executables are always linked statically, and accept the flag. Valid only for executable builds.
+
--optimization <release|dev|speed>
Select native optimization for executable, assembly, or object output. The default is release (-O2), which emits string retains and the retains and releases of dynamic values inline. dev uses -O0 and emits source locations for TypeScript and JavaScript breakpoints and stack frames. LLVM builds include source variable names and native storage descriptions; see Native debugging for the supported representations. macOS executable builds also produce an adjacent .dSYM bundle. speed is -O2 plus optimizations that trade binary size and build time for run-time speed: it emits the remaining reference counting inline and, on x86-64, starts every runtime function on a cache-line boundary. Expect larger executables and longer cold builds than release; how much faster a program runs depends on how much of its time is spent in the runtime. Where a runtime pack has no speed runtime, speed links the release runtime and keeps inline reference counting. Selecting speed never changes what release builds produce, and the two never share cached artifacts. Library profiles accept "optimization": "speed" for inline reference counting. In executables large enough to be divided into partitions, dev compiles each partition on its own, and release and speed add a step that lets partitions import small functions from each other; see Build parallelism.
@@ -160,6 +163,9 @@ 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.
+
--large-pages
+
On Linux, when the program starts, move its code onto 2 MiB transparent huge pages, as Node's --use-largepages=silent does. A large program's code then takes a few instruction TLB entries instead of hundreds, and runs faster. The move applies only when the system grants huge pages to memory that asks for them (/sys/kernel/mm/transparent_hugepage/enabled is always or madvise), and only to the code between the first and the last 2 MiB boundary of the executable, so small programs keep their code as it is. Opt in only where the trade-offs are acceptable: the move takes about 3 ms per 10 MiB of code at every start, all of the moved code stays resident and is not shared with other processes running the same executable, and profilers that read symbols from the executable file report the moved code as anonymous memory. If a step of the move fails, the program runs with its code where it was. 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.
@@ -170,7 +176,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
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.
@@ -262,6 +268,25 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time. +`scriptc build --profile-use` prints one line on stderr with how the profile matched the program's functions: + +``` +scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record +``` + +- **annotated:** functions optimized with the profile's counts. +- **mismatched:** functions whose control flow changed since the profile was collected. They are optimized without counts. +- **without a record:** functions the profile does not describe: new functions, and functions the profiled runs never reached. +- When the build reuses cached partitions whose counts were not recorded, the line ends with the number of those partitions. + +When more than 10% of the functions the profile describes (annotated and mismatched) are mismatched, the build also prints a warning: + +``` +scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again +``` + +The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts. + ## Target CPU By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it: @@ -351,6 +376,10 @@ For source breakpoints in Xcode or LLDB, build with --optimization=devscriptc run on wasm32-wasi Node.js 24 or later on PATH + + --static on glibc Linux + glibc's static libraries (libc6-dev on Debian and Ubuntu, glibc-static on Fedora) + --sanitize A C compiler and LLVM toolchain diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index db1193b98..d8719d9f3 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -83,6 +83,39 @@ Without a profile, a dynamic-tier function counts as likely to run often when a The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as `anonymous function`. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it. +## Dynamic slots + +A declared type that has no native representation does not fail the build. The field, parameter, return value, variable or expression that holds it becomes a checked dynamic slot: the parts of the type that compile keep their native layout, and the failing parts hold checked-dynamic values. Coverage lists the slots under the statement counts, and `scriptc build` prints one `SC6104` warning per slot. + +```console +$ cat plugins.ts +type Hook = (() => void) | AbortSignal | Map string>; + +interface Plugin { + name: string; + hook: Hook; +} + +const plugins: Plugin[] = [{ name: "log", hook: () => console.log("hook ran") }]; +for (const p of plugins) { + const hook = p.hook; + if (typeof hook === "function") hook(); + console.log(p.name); +} + +$ scriptc coverage plugins.ts + + statements analyzed 6 + compile statically 6 (100%) + in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) + dynamic slots 1 (declared types with no static lowering, held as checked-dynamic values; SC6104) + module global 'plugins' (declared 'Plugin[]') (dynamic pieces) plugins.ts:8:7 + + fully static — all analyzed statements compile to native code. +``` + +Here `Plugin` keeps its native `name` field, and only `hook` is checked-dynamic. Reading a dynamic value into typed code checks it against the type that reads it, so data that contradicts the declaration still throws a `TypeError`. Slots with the most dynamic operations are listed first. `--fail-on=dynamic-slots` fails a coverage run when any slot exists, and `--tier=static-only` reports the original errors instead. + ## Diagnostic codes Coverage and `scriptc build` report the same diagnostic codes. Common ones include: @@ -109,7 +142,7 @@ By default, `scriptc coverage` exits with status 0 whenever the program type-che $ scriptc coverage src/main.ts --fail-on=blockers ``` -`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, and `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often). A program that doesn't type-check always exits with status 1. +`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often), and `--fail-on=dynamic-slots` also fails on [dynamic slots](#dynamic-slots). A program that doesn't type-check always exits with status 1. Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set `SCRIPTC_NO_CACHE=1` to turn the cache off. diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index a6520b46e..0475e311e 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -34,7 +34,7 @@ Runtime pack and linker -> executable ## Build parallelism -Most of a build runs on one CPU core. The LLVM helper uses more cores only when it compiles an executable in partitions. +Most of a build runs on one CPU core. The LLVM helper uses more cores when it compiles an executable in partitions, and when it generates code for an optimized executable of one partition. ### Frontend @@ -62,7 +62,7 @@ The count depends only on the size of the LLVM text, so a program has the same p 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 four partitions uses at most four 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. How partitions are compiled depends on `--optimization`: @@ -71,6 +71,23 @@ How partitions are compiled depends on `--optimization`: Reading the LLVM text and dividing it into partitions, and the import step, run on one thread. +### Code generation of one partition + +A `release` or `speed` executable of one partition is optimized as one module on one thread. When its LLVM text is larger than 128 KiB, the helper then divides the optimized module for code generation and generates machine code for the parts in parallel, on the same pool. Each part holds a run of consecutive functions with about the same amount of code, so the executable keeps the functions in the same order. The module is optimized before it is divided, so each function's machine code is the same as from one thread, except that a jump to a function that another part also uses takes the longer form. When one function holds most of the code, the first part holds the whole module and the others are empty. + + + + + + + + + + +
LLVM textCode generation parts
Up to 128 KiB1
Over 128 KiB, up to 256 KiB4
Over 256 KiB, up to 1 MiB8
+ +Like partitions, the count depends only on the size of the LLVM text. A `dev` executable of one partition is compiled on one thread. + ### Rebuilds With two or more partitions, the helper keeps each partition's summary and object in the build cache (see `SCRIPTC_CACHE_DIR` in the [CLI reference](/docs/cli#environment-variables)). A rebuild after an edit compiles again only the partitions whose functions changed and, in `release` and `speed` builds, the partitions that import a changed function; the others come from the cache. Dividing the program, the import step, and linking run on every build. A cold build, with nothing cached, compiles every partition. @@ -134,8 +151,8 @@ Program objects define `main` and leave selected `scr_*` runtime functions undef ## Native runtime -- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit. -- **Concurrency:** An `async` function that suspends only at its own `await` expressions compiles to a stackless coroutine whose heap frame holds the values live across those awaits; other suspending `async` functions and async generators use stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux. +- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. When the compiler proves that a group of objects is released together, such as a tree that a recursive function builds and its caller only reads, the group is allocated in an arena and released at once, without freeing each object or collecting its cycles. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit. +- **Concurrency:** An `async` function or async generator that suspends only at its own `await` and `yield` expressions compiles to a stackless coroutine whose heap frame holds the values live across them; other suspending `async` functions and async generators use stackful fibers, as do debug builds, libraries, worker programs and Windows executables. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux. - **Networking:** Supported `net`, `http`, `https`, `tls`, `dgram`, and `dns` APIs use native implementations. TLS uses vendored mbedTLS. - **Numbers:** Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js. - **Regular expressions and Unicode:** The runtime uses QuickJS's regular-expression and Unicode libraries, linked into programs that use them. diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index cb6b93d0d..88b2ca5fc 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -48,10 +48,14 @@ title: "Limitations" ../limitations/types-and-class-definitions/unions.mdx +../limitations/types-and-class-definitions/dynamic-slots.mdx + ../limitations/types-and-class-definitions/null-and-optional-values.mdx ../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 diff --git a/docs/content/docs/platforms.mdx b/docs/content/docs/platforms.mdx index 96b91fc2a..72cd65a3c 100644 --- a/docs/content/docs/platforms.mdx +++ b/docs/content/docs/platforms.mdx @@ -49,7 +49,7 @@ $ file fib-linux fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped ``` -`x86_64-linux-gnu.2.36` works the same way. For Alpine and other musl-based systems, use `aarch64-linux-musl` or `x86_64-linux-musl`; these produce statically linked executables. +`x86_64-linux-gnu.2.36` works the same way. For Alpine and other musl-based systems, use `aarch64-linux-musl` or `x86_64-linux-musl`; these produce statically linked executables. On a glibc-based Linux machine, [`--static`](/docs/cli#options) links a static executable for the machine's own target. Linux executables use the native Linux implementations of the event loop (epoll), servers, TLS (with the system's CA bundle), and `fs.watch`, and they are tested against Node.js on Linux, including Alpine. diff --git a/docs/content/limitations/array-bounds-and-runtime-errors.mdx b/docs/content/limitations/array-bounds-and-runtime-errors.mdx index 7490a1e15..082dd37f3 100644 --- a/docs/content/limitations/array-bounds-and-runtime-errors.mdx +++ b/docs/content/limitations/array-bounds-and-runtime-errors.mdx @@ -11,6 +11,6 @@ Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process. Deep recursion in Linux and macOS executables throws Node's catchable `RangeError: Maximum call stack size exceeded`, on the main thread, in worker threads, and inside async functions and generators. -The depth at which it is thrown differs from Node: the main thread uses the process stack limit (usually 8 MiB), worker threads get an 8 MiB stack, and async functions and generators run on 512 KiB stacks, so native code usually recurses deeper than Node on threads and somewhat less deeply inside async bodies. +The depth at which it is thrown differs from Node: the main thread uses the process stack limit (usually 8 MiB), worker threads get an 8 MiB stack, async functions and generators that keep a fiber run on 512 KiB stacks, and stackless ones run on the stack that resumes them, so native code usually recurses deeper than Node on threads and somewhat less deeply inside fiber bodies. Recursion inside runtime library operations, such as serializing deeply nested data, is not checked. Windows executables, WASI builds and library builds do not check the stack, and exhausting it there terminates the process. 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-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index efd45af2f..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. 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/dynamic-slots.mdx b/docs/content/limitations/types-and-class-definitions/dynamic-slots.mdx new file mode 100644 index 000000000..102c7ce0a --- /dev/null +++ b/docs/content/limitations/types-and-class-definitions/dynamic-slots.mdx @@ -0,0 +1,5 @@ +- **Dynamic slots:** A field, parameter, return value, variable or expression whose declared type has no native representation compiles as a checked dynamic slot instead of failing the build. + The parts of the type that compile keep their native layout, and only the failing parts hold checked-dynamic values. + Operations on those values run in the dynamic tier, and reading one into typed code checks it against the type that reads it, so a value that contradicts its declaration throws a `TypeError` when typed code first uses it rather than when it is stored. + Each slot is an `SC6104` warning; `scriptc coverage --fail-on=dynamic-slots` fails on them, and `--tier=static-only` turns them back into build errors. + Values that scriptc cannot produce at all, such as `WeakRef` instances, still fail where they are created. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 2f90447ad..56e26c232 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 8060af796..aac6cfb14 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -52431,6 +52431,7 @@ "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", @@ -54483,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" @@ -54508,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" }, @@ -54531,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" }, @@ -54554,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" }, @@ -54600,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" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 5eb4c1b11..033e199c7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -222,8 +222,10 @@ { "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"]}, diff --git a/native/llvm-codegen/README.md b/native/llvm-codegen/README.md index 02ac6e3ef..7305db21d 100644 --- a/native/llvm-codegen/README.md +++ b/native/llvm-codegen/README.md @@ -33,7 +33,9 @@ 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. + +With `--split codegen`, several outputs divide only code generation. The helper optimizes the module whole, as it does for a single output, then divides its functions in module order into runs of about the same instruction count, one per output, and generates code for every output on its own thread. Linking the outputs in order lays the functions out as one output would. A local definition that another output uses becomes hidden so the objects can reference each other; the others stay local. Every output after the first is generated from a copy of the optimized module read back from bitcode with its use-list order, so each function's machine code matches a single output except for jumps to definitions that another output also uses. When one run would hold more than three quarters of the module's instructions, the first output holds the whole module and the other outputs are empty objects. Placement depends only on the optimized module and the output count. `--split codegen` requires an optimized build and no `--cache-dir`; scriptc passes it for optimized executables with more than 128 KiB of LLVM text that stay one partition. 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. diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp index 4a7e2333b..d3d868fae 100644 --- a/native/llvm-codegen/src/emit.cpp +++ b/native/llvm-codegen/src/emit.cpp @@ -6,8 +6,11 @@ #include "runtime-import.h" #include "target.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/Analysis/RuntimeLibcallInfo.h" @@ -48,6 +51,7 @@ #include "llvm/TargetParser/Triple.h" #include "llvm/Transforms/Utils/SplitModule.h" +#include #include #include #include @@ -64,6 +68,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; @@ -97,6 +104,8 @@ std::optional parseEmitOptions(int Argc, char **Argv) { Options.ProfileGenerate = Value.str(); else if (Arg == "--profile-use") Options.ProfileUse = Value.str(); + else if (Arg == "--split" && Value == "codegen") + Options.SplitCodegen = true; else return std::nullopt; } @@ -173,6 +182,22 @@ 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 @@ -193,8 +218,8 @@ struct UnnameStructTypes : PassInfoMixin { // 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 +// it has no record of (new since it was collected, or never run in training +// when the profile was merged with --sparse). A partition reused from the cache without its counts is counted // in Unknown. struct ProfileMatch { std::atomic Annotated{0}; @@ -852,6 +877,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), @@ -1023,6 +1049,251 @@ int emitPartitions(const EmitOptions &Options) { return 0; } +// An optimized module prepared for split code generation. +struct CodegenSplit { + // The module as bitcode, with the order of every use list. + SmallVector Bitcode; + // The output that holds each definition, by name. + StringMap PartOf; +}; + +// Divides an optimized module's definitions among Count outputs for code +// generation. Functions are divided in module order into runs of about the +// same instruction count, so linking the outputs in order lays the functions +// out as one module would; a function that would take its run past an even +// share starts the next run. A local definition that another output uses +// becomes hidden so the outputs can reference each other; the others stay +// local. Variables and aliases go with output 0 unless a comdat, an aliasee +// or an ifunc resolver places them. When one run would hold more than three +// quarters of the instructions, dividing cannot shorten code generation: the +// result is empty and M is unchanged. Placement depends only on the module. +std::optional splitForCodegen(Module &M, size_t Count) { + uint64_t Total = 0; + for (const Function &F : M) + if (!F.isDeclaration()) + Total += F.getInstructionCount() + 1; + DenseMap Part; + DenseMap ComdatPart; + std::vector Load(Count, 0); + uint64_t Share = (Total + Count - 1) / Count; + size_t Current = 0; + uint64_t InRun = 0; + for (Function &F : M) { + if (F.isDeclaration()) + continue; + uint64_t Size = F.getInstructionCount() + 1; + if (InRun > 0 && InRun + Size > Share && Current + 1 < Count) { + ++Current; + InRun = 0; + } + size_t Target = Current; + if (const Comdat *C = F.getComdat()) { + auto [It, Inserted] = ComdatPart.try_emplace(C, Target); + Target = It->second; + } + Part[&F] = Target; + Load[Target] += Size; + InRun += Size; + } + if (*std::max_element(Load.begin(), Load.end()) * 4 > Total * 3) + return std::nullopt; + auto PartOf = [&](const GlobalValue *GV) -> size_t { + if (const GlobalObject *Root = GV->getAliaseeObject()) { + if (const auto *IFunc = dyn_cast(Root)) + Root = IFunc->getResolverFunction(); + if (Root != nullptr && Root != GV) + GV = Root; + } + if (auto It = Part.find(GV); It != Part.end()) + return It->second; + if (const auto *GO = dyn_cast(GV)) + if (const Comdat *C = GO->getComdat()) + if (auto It = ComdatPart.find(C); It != ComdatPart.end()) + return It->second; + return 0; + }; + // Whether a user of GV, through any constants, lies outside output Home. + auto UsedElsewhere = [&](const GlobalValue &GV, size_t Home) { + SmallVector Work(GV.users()); + SmallPtrSet Seen; + while (!Work.empty()) { + const User *U = Work.pop_back_val(); + if (!Seen.insert(U).second) + continue; + if (const auto *I = dyn_cast(U)) { + if (PartOf(I->getFunction()) != Home) + return true; + } else if (const auto *G = dyn_cast(U)) { + if (PartOf(G) != Home) + return true; + } else { + append_range(Work, U->users()); + } + } + return false; + }; + for (GlobalValue &GV : M.global_values()) { + if (GV.hasLocalLinkage() && UsedElsewhere(GV, PartOf(&GV))) { + GV.setLinkage(GlobalValue::ExternalLinkage); + GV.setVisibility(GlobalValue::HiddenVisibility); + } + if (!GV.hasName()) + GV.setName("scr.split"); + } + CodegenSplit Split; + for (const GlobalValue &GV : M.global_values()) + if (!GV.isDeclaration()) + Split.PartOf[GV.getName()] = PartOf(&GV); + raw_svector_ostream Stream(Split.Bitcode); + WriteBitcodeToFile(M, Stream, /*ShouldPreserveUseListOrder=*/true); + return Split; +} + +// Reduces P, a copy of the split module, to the definitions of output I: +// the other definitions become declarations, and declarations that nothing +// uses are removed. A declaration nothing uses would still emit an untyped +// undefined symbol for its visibility, which some linkers reject against a +// thread-local definition. Removing definitions keeps the order of the +// remaining uses, so each function of the output reaches code generation +// as it would in the whole module. +void keepCodegenPart(Module &P, const StringMap &PartOf, size_t I) { + auto Elsewhere = [&](const GlobalValue &GV) { + auto It = PartOf.find(GV.getName()); + return It != PartOf.end() && It->second != I; + }; + // Aliases and ifuncs cannot be declarations, so a declaration replaces + // them. + SmallVector Indirect; + for (GlobalAlias &GA : P.aliases()) + if (Elsewhere(GA)) + Indirect.push_back(&GA); + for (GlobalIFunc &GI : P.ifuncs()) + if (Elsewhere(GI)) + Indirect.push_back(&GI); + for (GlobalValue *GV : Indirect) { + GlobalValue *Declaration; + if (auto *Type = dyn_cast(GV->getValueType())) + Declaration = Function::Create(Type, GlobalValue::ExternalLinkage, + GV->getAddressSpace(), "", &P); + else + Declaration = new GlobalVariable( + P, GV->getValueType(), /*isConstant=*/false, + GlobalValue::ExternalLinkage, nullptr, "", nullptr, + GV->getThreadLocalMode(), GV->getAddressSpace()); + Declaration->setVisibility(GV->getVisibility()); + Declaration->setDSOLocal(GV->isDSOLocal()); + Declaration->takeName(GV); + GV->replaceAllUsesWith(Declaration); + GV->eraseFromParent(); + } + for (Function &F : P) + if (!F.isDeclaration() && Elsewhere(F)) { + F.deleteBody(); + F.setComdat(nullptr); + } + for (GlobalVariable &GV : P.globals()) + if (!GV.isDeclaration() && Elsewhere(GV)) { + GV.setInitializer(nullptr); + GV.setComdat(nullptr); + GV.setLinkage(GlobalValue::ExternalLinkage); + } + if (I != 0) + P.setModuleInlineAsm(""); + // Constant expressions that only the removed bodies used remain as users. + for (GlobalVariable &GV : make_early_inc_range(P.globals())) + if (GV.isDeclaration()) { + GV.removeDeadConstantUsers(); + if (GV.use_empty()) + GV.eraseFromParent(); + } + for (Function &F : make_early_inc_range(P)) + if (F.isDeclaration()) { + F.removeDeadConstantUsers(); + if (F.use_empty()) + F.eraseFromParent(); + } +} + +// Optimizes M whole, as a single output would, then divides its definitions +// among the outputs and generates code for each on its own thread. The first +// output is generated from M with Machine; the others from copies of M read +// back from bitcode, which keeps the order of every use list, so each +// function's code matches a single-output build except where it refers to a +// definition in another output. Placement depends only +// on the optimized module and the output count. When splitForCodegen keeps +// the module whole, the other outputs are empty objects. +std::optional +emitSplitCodegen(const EmitOptions &Options, Module &M, TargetMachine &Machine, + OptimizationLevel Level, ProfileMatch &Match) { + bool Annotating = !Options.ProfileUse.empty(); + if (Annotating) + M.getContext().setDiagnosticHandler( + std::make_unique(Match)); + { + Pipeline P(Machine, Level, profileOptions(Options)); + if (Annotating) + P.countAnnotated(Match.Annotated); + P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); + } + if (std::optional Failure = + verify(M, "post_optimization_verification_failed")) + return Failure; + size_t Count = Options.Outputs.size(); + std::optional Split = splitForCodegen(M, Count); + std::string Identifier = M.getModuleIdentifier(); + std::string SourceFileName = M.getSourceFileName(); + Triple TargetTriple = M.getTargetTriple(); + DataLayout Layout = M.getDataLayout(); + std::vector>> Temporaries(Count); + SharedFailure Failures; + forEachPartition(Count, [&](size_t I) { + if (I == 0) { + if (Split) + keepCodegenPart(M, Split->PartOf, 0); + if (std::optional Failure = + generateCode(M, Machine, CodeGenFileType::ObjectFile, + Options.Outputs[0], Temporaries[0])) + Failures.set(std::move(*Failure)); + return; + } + std::string LookupError; + std::unique_ptr PartMachine = + createTargetMachine(Options.Target, Options.OptLevel, LookupError); + if (!PartMachine) + return Failures.set({"target_machine_failed", LookupError}); + LLVMContext Context; + std::unique_ptr Part; + if (Split) { + Expected> Parsed = parseBitcodeFile( + MemoryBufferRef( + StringRef(Split->Bitcode.data(), Split->Bitcode.size()), "part"), + Context); + if (!Parsed) + return Failures.set( + {"partition_failed", toString(Parsed.takeError())}); + Part = std::move(*Parsed); + keepCodegenPart(*Part, Split->PartOf, I); + } else { + Part = std::make_unique(Identifier, Context); + Part->setSourceFileName(SourceFileName); + Part->setTargetTriple(TargetTriple); + Part->setDataLayout(Layout); + } + if (std::optional Failure = + generateCode(*Part, *PartMachine, CodeGenFileType::ObjectFile, + Options.Outputs[I], Temporaries[I])) + return Failures.set(std::move(*Failure)); + }); + std::optional Failure = Failures.take(); + for (size_t I = 0; I < Count && !Failure; ++I) + Failure = publish(Temporaries[I][0], Options.Outputs[I]); + if (Failure) + for (const std::vector> &Paths : Temporaries) + for (const SmallString<256> &Temporary : Paths) + sys::fs::remove(Temporary); + return Failure; +} + } // namespace int emit(const EmitOptions &Options) { @@ -1069,7 +1340,14 @@ int emit(const EmitOptions &Options) { if (std::optional Failure = inspectProfile(Options)) return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); - if (Options.Outputs.size() > 1) + if (Options.SplitCodegen && (Options.Outputs.size() < 2 || + Options.OptLevel == "0" || + !Options.CacheDirectory.empty())) + return reportError("invalid_split", + "--split codegen requires several outputs, an " + "optimized build, and no cache directory", + Options.DiagnosticFormat); + if (Options.Outputs.size() > 1 && !Options.SplitCodegen) return emitPartitions(Options); SMDiagnostic ParseDiagnostic; @@ -1127,6 +1405,16 @@ int emit(const EmitOptions &Options) { CodeGenFileType Type = Options.FileType == "obj" ? CodeGenFileType::ObjectFile : CodeGenFileType::AssemblyFile; + if (Options.SplitCodegen) { + ProfileMatch Match; + if (std::optional Failure = + emitSplitCodegen(Options, *Mod, *Machine, Level, Match)) + return reportError(Failure->Code, Failure->Message, + Options.DiagnosticFormat); + if (!Options.ProfileUse.empty()) + reportProfileMatch(Match); + return 0; + } std::vector> Temporaries; ProfileMatch Match; std::optional Failure = diff --git a/native/llvm-codegen/src/emit.h b/native/llvm-codegen/src/emit.h index 39b8c2463..b62a9d01c 100644 --- a/native/llvm-codegen/src/emit.h +++ b/native/llvm-codegen/src/emit.h @@ -26,6 +26,9 @@ struct EmitOptions { std::string ProfileGenerate; // Indexed profile (.profdata) that guides optimization. Empty: none. std::string ProfileUse; + // With several outputs: optimize the module whole and divide only its code + // generation among the outputs. + bool SplitCodegen = false; }; std::optional parseEmitOptions(int Argc, char **Argv); diff --git a/native/llvm-codegen/src/partition.cpp b/native/llvm-codegen/src/partition.cpp index 93b9af4a3..ef5230129 100644 --- a/native/llvm-codegen/src/partition.cpp +++ b/native/llvm-codegen/src/partition.cpp @@ -98,7 +98,7 @@ void placeGroups(std::vector &Symbols, ArrayRef Roots, StringMap GroupBytes; for (unsigned I = 0; I < Symbols.size(); ++I) { const Symbol &S = Symbols[I]; - if (S.Kind != SymbolKind::Function) + if (S.Kind != SymbolKind::Function || S.Partition == NoPartition) continue; Total += Bytes(S); StringRef Group = Symbols[Roots[I]].Group; @@ -119,8 +119,10 @@ void placeGroups(std::vector &Symbols, ArrayRef Roots, 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 (S.Kind != SymbolKind::Function || Group.empty()) + if (Group.empty()) continue; uint64_t Chunks = 1; while (Chunks * Chunk < GroupBytes[Group]) @@ -534,64 +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; - } - } - std::vector Roots(Symbols.size()); - 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; - } - Roots[I] = Root; - Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count; - } - placeGroups(Symbols, Roots, Count); - for (const Symbol &S : Symbols) { - if (S.Kind != SymbolKind::Function) - continue; - 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 @@ -606,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 35ef4c543..dc000d751 100644 --- a/native/llvm-codegen/src/partition.h +++ b/native/llvm-codegen/src/partition.h @@ -24,11 +24,15 @@ struct ProgramPartitions { // 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 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. +// 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 a3cc42fc7..9b0213b44 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 1a8ee5d2c..1d4921f48 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -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 0cb24b908..dca236b32 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -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 3a1c7488d..1459caa1d 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -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 e671e926b..3703c0059 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -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 2b56308fd..6d70361f9 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -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 4ca4c2837..0c12fc60d 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -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 d734c0807..8872d2f15 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -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/scripts/install-native.mjs b/packages/cli/scripts/install-native.mjs index 4db76cc24..e8317d341 100644 --- a/packages/cli/scripts/install-native.mjs +++ b/packages/cli/scripts/install-native.mjs @@ -93,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 506e4cf6b..3ade659c1 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -74,7 +74,11 @@ async function tryFastPath(): Promise { values["target-cpu"] !== undefined || (values["external-types"] ?? []).length > 0 || // The full CLI validates the Node island policy flags. - hasNodeIslandFlags(values) + hasNodeIslandFlags(values) || + // The full compiler writes the representation report and keys the heap + // census build (backend/llvm/repr-report.ts, heap-census.ts). + (process.env["SCRIPTC_REPR_REPORT"] ?? "") !== "" || + (process.env["SCRIPTC_HEAP_CENSUS"] ?? "") !== "" ) return null; const backend = values.backend; @@ -140,10 +144,17 @@ async function tryFastPath(): Promise { backend: "llvm", ...(optimization === "dev" || optimization === "speed" ? { optimization } : {}), ...(values.strip ? { strip: true as const } : {}), + ...(values.static ? { staticLink: true as const } : {}), ...(values["detached-exit"] ? { detachedExit: true as const } : {}), + ...(values["large-pages"] ? { largePages: 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" @@ -219,7 +230,9 @@ async function coverageFastPath( (failOn !== undefined && failOn !== "blockers" && failOn !== "divergences") || values.emit !== undefined || values.strip || + values.static || values["detached-exit"] || + values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values.lib || @@ -231,6 +244,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/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index ee8027765..eef8e97a0 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -198,3 +198,57 @@ test.skipIf(process.platform === "win32")( }, 120_000, ); + +test.skipIf(process.platform !== "linux")( + "a warm executable cache still builds the heap census and writes the representation report", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-diagnostic-cache-")); + const cacheRoot = join(dir, "cache"); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + try { + await mkdir(cacheRoot, { mode: 0o700 }); + // `new` and construction through a class value both count. + await writeFile( + entry, + [ + "class Item { constructor(readonly id: number) {} }", + "const kind: typeof Item = Item;", + "const items = [new Item(1), new kind(2), new kind(3)];", + "console.log(items.length);", + "", + ].join("\n"), + ); + const build = async (env: NodeJS.ProcessEnv = {}): Promise => + ( + await execFileAsync( + process.execPath, + ["--import", tsxLoader, cliEntry, "build", entry, "-o", outPath], + { + env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env }, + maxBuffer: 4 * 1024 * 1024, + }, + ) + ).stderr; + expect(await build()).toContain("scriptc lowering"); + expect(await build()).not.toContain("scriptc lowering"); + + // The census build is its own cache entry. + const census = join(dir, "census.tsv"); + await build({ SCRIPTC_HEAP_CENSUS: "1" }); + const { stdout } = await execFileAsync(outPath, [], { + env: { ...process.env, SCRIPTC_HEAP_CENSUS: census }, + }); + expect(stdout).toBe("3\n"); + expect(await readFile(census, "utf8")).toMatch(/^new\tItem\t\t3\t[1-9]\d*$/m); + + // A report is written even when the executable is cached. + const report = join(dir, "repr.tsv"); + await build({ SCRIPTC_REPR_REPORT: report }); + expect(await readFile(report, "utf8")).toMatch(/^field\tItem\tid\t/m); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 600_000, +); diff --git a/packages/cli/test/native-install.test.ts b/packages/cli/test/native-install.test.ts index 5e51a2447..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"; @@ -104,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 24db2c4c2..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: 46, marker: "scr_runtime_abi_v46" }, + 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_v46"); + 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 b9eb94843..45d5c55b8 100644 --- a/packages/cli/test/output-options.test.ts +++ b/packages/cli/test/output-options.test.ts @@ -46,8 +46,12 @@ describe("output option compatibility", () => { [{ emit: "llvm", windowsSubsystem: "gui" }, /windows-subsystem/], [{ emit: "obj", strip: true }, /strip/], [{ emit: "llvm", strip: true }, /strip/], + [{ emit: "obj", staticLink: true }, /--static/], + [{ emit: "llvm", staticLink: true }, /--static/], [{ emit: "obj", detachedExit: true }, /detached-exit/], [{ emit: "llvm", detachedExit: true }, /detached-exit/], + [{ emit: "obj", largePages: true }, /large-pages/], + [{ emit: "llvm", largePages: true }, /large-pages/], [{ emit: "asm", cpuDispatch: true }, /cpu-dispatch/], [{ emit: "obj", profileGenerate: "/tmp/%p.profraw" }, /profile-generate/], [{ emit: "llvm", profileUse: "/tmp/a.profdata" }, /profile-use/], 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/static-link.test.ts b/packages/cli/test/static-link.test.ts new file mode 100644 index 000000000..68fbda064 --- /dev/null +++ b/packages/cli/test/static-link.test.ts @@ -0,0 +1,138 @@ +import { execFile, execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, isAbsolute, 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 glibcHost = + process.platform === "linux" && + typeof (process.report?.getReport() as { header?: { glibcVersionRuntime?: unknown } })?.header + ?.glibcVersionRuntime === "string"; + +/** Whether the host's clang finds glibc's static C library. */ +function hostHasStaticLibc(): boolean { + try { + const path = execFileSync("clang", ["-print-file-name=libc.a"], { encoding: "utf8" }).trim(); + return isAbsolute(path) && existsSync(path); + } catch { + return false; + } +} + +/** The ELF type and whether the executable names a program interpreter. */ +async function elfLinkage(path: string): Promise<{ type: number; interpreter: boolean }> { + const bytes = await readFile(path); + const type = bytes.readUInt16LE(16); + const phoff = Number(bytes.readBigUInt64LE(32)); + const phentsize = bytes.readUInt16LE(54); + const phnum = bytes.readUInt16LE(56); + let interpreter = false; + for (let i = 0; i < phnum; i++) { + if (bytes.readUInt32LE(phoff + i * phentsize) === 3 /* PT_INTERP */) interpreter = true; + } + return { type, interpreter }; +} + +async function withProject(run: (dir: string, env: NodeJS.ProcessEnv) => Promise) { + const dir = await mkdtemp( + join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-static-"), + ); + try { + return await run(dir, { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function cli(args: string[], env: NodeJS.ProcessEnv, bootstrap = false) { + return execFileAsync( + process.execPath, + [...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]), ...args], + { env, maxBuffer: 4 * 1024 * 1024 }, + ); +} + +test.skipIf(!glibcHost || !hostHasStaticLibc())( + "--static links a glibc executable as static PIE without mixing cache variants", + async () => { + await withProject(async (dir, env) => { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + // The NSS module file name in a string of the program's own is no lookup. + await writeFile( + entry, + 'console.log("static-ok", [1, 2, 3].map((n) => n * 2).join(","), "/lib/libnss_files.so.2");\n', + ); + const build = async (staticLink: boolean, bootstrap = false) => { + await cli( + ["build", entry, "-o", outPath, ...(staticLink ? ["--static"] : [])], + env, + bootstrap, + ); + expect((await execFileAsync(outPath)).stdout).toBe( + "static-ok 2,4,6 /lib/libnss_files.so.2\n", + ); + return elfLinkage(outPath); + }; + // ET_DYN with a program interpreter: the default dynamic PIE link. + expect(await build(false)).toEqual({ type: 3, interpreter: true }); + // ET_DYN without one: static PIE. + expect(await build(true)).toEqual({ type: 3, interpreter: false }); + expect(await build(true, true)).toEqual({ type: 3, interpreter: false }); + expect(await build(false, true)).toEqual({ type: 3, interpreter: true }); + }); + }, + 180_000, +); + +test.skipIf(!glibcHost || !hostHasStaticLibc())( + "--static refuses a glibc executable that includes user name lookups", + async () => { + await withProject(async (dir, env) => { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + await writeFile( + entry, + 'import { userInfo } from "node:os";\nconsole.log(userInfo().username);\n', + ); + const failure = await cli(["build", entry, "-o", outPath, "--static"], env).then( + () => null, + (error: { code?: number; stderr?: string }) => error, + ); + expect(failure?.code).toBe(1); + expect(failure?.stderr).toContain("error SC3002: --static cannot link this program"); + expect(existsSync(outPath)).toBe(false); + // The same program links without --static. + await cli(["build", entry, "-o", outPath], env); + expect((await execFileAsync(outPath)).stdout).not.toBe(""); + }); + }, + 180_000, +); + +test.skipIf(process.platform === "linux")( + "--static is refused for executables that do not target Linux", + async () => { + await withProject(async (dir, env) => { + const entry = join(dir, "main.ts"); + await writeFile(entry, 'console.log("x");\n'); + const failure = await cli(["build", entry, "-o", join(dir, "program"), "--static"], env).then( + () => null, + (error: { code?: number; stderr?: string }) => error, + ); + expect(failure?.code).toBe(1); + expect(failure?.stderr).toContain("--static is only supported for Linux executables"); + }); + }, + 180_000, +); diff --git a/packages/cli/test/target-cpu.test.ts b/packages/cli/test/target-cpu.test.ts index 5bc6717c3..dec7de15b 100644 --- a/packages/cli/test/target-cpu.test.ts +++ b/packages/cli/test/target-cpu.test.ts @@ -54,7 +54,7 @@ test.runIf(process.platform === "linux" && process.arch === "x64")( 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); + expect(v3).toMatch(/^attributes #0 = \{ uwtable "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); 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 ee2208bd9..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 @@ -1292,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 @@ -1525,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 @@ -1548,1188 +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 | 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 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 | 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"; -} - -/* 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 @@ -2791,2063 +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, 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"; -} - -/* 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"; -} - -/* 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 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"; -} - -/* 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), @@ -5045,2484 +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, 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"; -} - -/* 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; - /* 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 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"; -} - -/* 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" { @@ -7544,808 +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. 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"; -} -/* 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/native/cli.layout.gz b/packages/compiler/native/cli.layout.gz new file mode 100644 index 000000000..84a2558b5 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 aa9398b5f..4480e4ecb 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 24d330cc0..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. */ diff --git a/packages/compiler/native/ts7-process.test.c b/packages/compiler/native/ts7-process.test.c index b6d760abd..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 diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 03af418a5..3237def0a 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -40,10 +40,13 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.9", + "tsc-ts": "0.0.10", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, + "devDependencies": { + "typescript-7.1": "npm:typescript@7.1.0-beta" + }, "optionalDependencies": { "@scriptc/llvm-darwin-arm64": "workspace:*", "@scriptc/llvm-darwin-x64": "workspace:*", 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 a3350eb89..1d95285b4 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -5,10 +5,11 @@ import { hasForeignFfiCallback } from "./ffi-callbacks.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; import { youngNurseryRequested } from "./young-nursery.js"; -export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { +export function executableLinkFeatures(mod: IrModule, largePages = false): NativeLinkFeatures { const features = moduleRuntimeFeatures(mod); return { ...(mod.workers ? { workers: true } : {}), + ...(largePages ? { largePages: true } : {}), ...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}), regex: features.regex, copying: features.copying, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 521f1d53e..14ffdff28 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -166,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.ts b/packages/compiler/src/backend/link-plan-core.ts index fc05d2961..f810a4a17 100644 --- a/packages/compiler/src/backend/link-plan-core.ts +++ b/packages/compiler/src/backend/link-plan-core.ts @@ -2,6 +2,7 @@ import type { NativeOptimization } from "./optimization.js"; import { executableOptimizationLinkerArgs, + executableStaticLinkerArgs, executableStripLinkerArgs, windowsSubsystemLinkerArgs, type NativeTargetSpec, @@ -28,6 +29,8 @@ export function executableLinkInputs(options: { runtimeSystemLibraries: readonly string[]; optimization: NativeOptimization; strip?: boolean; + /** Link the C library and the other system libraries statically (`--static`). */ + staticLink?: boolean; windowsSubsystem?: WindowsSubsystem; /** Instrumented for profile-guided optimization. */ profileGenerate?: boolean; @@ -65,6 +68,7 @@ export function executableLinkInputs(options: { ), ...executableOptimizationLinkerArgs(options.target.platform, options.optimization), ...executableStripLinkerArgs(options.target.platform, options.strip ?? false), + ...executableStaticLinkerArgs(options.target, options.staticLink ?? false), ...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem), ...profileGenerateLinkerArgs(options.profileGenerate ?? false), ], @@ -75,7 +79,7 @@ export function executableLinkInputs(options: { * 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. */ + * loading libm.so when it links with lld (see inputsWithoutLld). */ export function systemLibraryLinkerArgs( target: Pick, names: readonly string[], diff --git a/packages/compiler/src/backend/link-plan.ts b/packages/compiler/src/backend/link-plan.ts index f0be965be..73139d22a 100644 --- a/packages/compiler/src/backend/link-plan.ts +++ b/packages/compiler/src/backend/link-plan.ts @@ -28,6 +28,8 @@ export interface NativeLinkPlan { programObjectDependencies: NativeArtifactDependency[]; runtimePack: RuntimePackSelection; darwinDebugSymbols?: boolean; + /** Linked with `--static`: the linked executable is checked before use. */ + staticLink?: boolean; } export async function createNativeLinkPlan(options: { @@ -39,6 +41,7 @@ export async function createNativeLinkPlan(options: { ffi: FfiProfile | null; optimization: NativeOptimization; strip?: boolean; + staticLink?: boolean; windowsSubsystem?: WindowsSubsystem; profileGenerate?: boolean; programObjectDependencies?: readonly NativeArtifactDependency[]; @@ -70,6 +73,7 @@ export async function createNativeLinkPlan(options: { runtimeSystemLibraries: runtimePack.systemLibraries, optimization: options.optimization, ...(options.strip === undefined ? {} : { strip: options.strip }), + ...(options.staticLink === true ? { staticLink: true } : {}), ...(options.windowsSubsystem === undefined ? {} : { windowsSubsystem: options.windowsSubsystem }), @@ -78,5 +82,6 @@ export async function createNativeLinkPlan(options: { dependencyPaths: [...runtimePack.dependencyPaths, ...(options.ffi?.libraries ?? [])], programObjectDependencies: [...(options.programObjectDependencies ?? [])], runtimePack, + ...(options.staticLink === true ? { staticLink: true } : {}), }; } diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts index c4f04133e..fe476360d 100644 --- a/packages/compiler/src/backend/linker.ts +++ b/packages/compiler/src/backend/linker.ts @@ -7,11 +7,13 @@ import { execFile } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { existsSync, realpathSync, statSync } from "node:fs"; -import { mkdtemp, rename, rm, stat } from "node:fs/promises"; +import { mkdtemp, readFile, rename, rm, stat } from "node:fs/promises"; import { delimiter } from "node:path"; import { basename, dirname, isAbsolute, join, resolve } from "node:path"; import { promisify } from "node:util"; import { CcCompileError, subprocessFailureDetail } from "./native/process.js"; +import { NativeCodegenError } from "./native-codegen-core.js"; +import { staticExecutableRefusal, staticLinkFailureNote, staticLinkMapArgs } from "./targets.js"; import { nativeArtifactDependenciesStillMatch } from "./native/artifact-stamps.js"; import { parseLinkTraceFiles } from "./native/linker-fingerprint.js"; import { @@ -21,7 +23,7 @@ import { import { type NativeArtifactDependency } from "./native/contracts.js"; import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js"; import type { NativeLinkPlan } from "./link-plan.js"; -import { lldLinkerArgs } from "./lld.js"; +import { inputsWithoutLld, lldLinkerArgs } from "./lld.js"; import { systemLibraryLinkerArgs } from "./link-plan-core.js"; import { createDarwinDebugSymbols, @@ -238,21 +240,28 @@ export async function linkNativeExecutable( join(dirname(plan.outputPath), ".scriptc-runtime-pack-link-"), ); const privateOut = join(privateOutRoot, basename(plan.outputPath)); + const linkMap = join(privateOutRoot, "link.map"); + const mapArgs = staticLinkMapArgs(plan.target, plan.staticLink === true, linkMap); let stagedRoot: string | null = null; 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), - ...systemLibraryLinkerArgs(plan.target, plan.systemLibraries), - "-o", - privateOut, - ]; + const linkArgs = (withLld: boolean): string[] => { + const inputs = plan.inputs.map((input) => staged.replacements.get(input) ?? input); + return [ + ...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []), + ...(withLld ? lld : []), + ...plan.driverFlags, + ...mapArgs, + ...(withLld ? inputs : inputsWithoutLld(plan.target.platform, inputs)), + ...systemLibraryLinkerArgs(plan.target, plan.systemLibraries), + "-o", + privateOut, + ]; + }; + const args = linkArgs(lld.length !== 0); // The pack snapshots bracket both verification passes and private staging; // the program-object snapshot begins before helper emission. Snapshot the // remaining cache-bearing inputs before the linker consumes them, then @@ -285,13 +294,14 @@ export async function linkNativeExecutable( // 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)), - ); + await execFileAsync(linker, linkArgs(false)); }); const output = await stat(privateOut); if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable"); + if (mapArgs.length !== 0) { + const refusal = staticExecutableRefusal(plan.target, true, await readFile(linkMap, "utf8")); + if (refusal !== null) throw new NativeCodegenError("SC3002", refusal); + } if (plan.darwinDebugSymbols) { await createDarwinDebugSymbols(privateOut); await installDarwinDebugSymbols(await readDarwinDebugSymbols(privateOut), plan.outputPath); @@ -311,12 +321,18 @@ export async function linkNativeExecutable( await options.onArtifactReady({ dependencies: preLinkDependencies }).catch(() => undefined); } } catch (error) { - if (error instanceof CcCompileError || error instanceof RuntimePackError) throw error; + if ( + error instanceof CcCompileError || + error instanceof RuntimePackError || + error instanceof NativeCodegenError + ) + throw error; const detail = subprocessFailureDetail(error); throw new CcCompileError( linker, detail, - `${linker} failed linking ${basename(plan.outputPath)} from the precompiled runtime pack.\n${detail}`, + `${linker} failed linking ${basename(plan.outputPath)} from the precompiled runtime pack.\n${detail}` + + staticLinkFailureNote(plan.target, plan.staticLink === true), ); } finally { await Promise.all([ diff --git a/packages/compiler/src/backend/lld.test.ts b/packages/compiler/src/backend/lld.test.ts index b0a466cf6..048f2ab94 100644 --- a/packages/compiler/src/backend/lld.test.ts +++ b/packages/compiler/src/backend/lld.test.ts @@ -2,7 +2,7 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; -import { driverFindsLld, lldLinkerArgs } from "./lld.js"; +import { driverFindsLld, inputsWithoutLld, lldLinkerArgs } from "./lld.js"; const directories: string[] = []; afterEach(() => { @@ -59,3 +59,10 @@ test.skipIf(process.platform === "win32")( expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]); }, ); + +test("Linux links without lld leave out the glibc math unit", () => { + const inputs = ["/pack/scr_lib.o", "/pack/scr_glibc_math.o", "/tmp/program.o"]; + expect(inputsWithoutLld("linux", inputs)).toEqual(["/pack/scr_lib.o", "/tmp/program.o"]); + expect(inputsWithoutLld("darwin", inputs)).toEqual(inputs); + expect(inputsWithoutLld("windows", inputs)).toEqual(inputs); +}); diff --git a/packages/compiler/src/backend/lld.ts b/packages/compiler/src/backend/lld.ts index 790e07062..7a9210407 100644 --- a/packages/compiler/src/backend/lld.ts +++ b/packages/compiler/src/backend/lld.ts @@ -1,5 +1,5 @@ import { realpathSync, statSync } from "node:fs"; -import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; +import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path"; function isFile(path: string): boolean { try { @@ -47,3 +47,21 @@ export function lldLinkerArgs( ? ["-fuse-ld=lld"] : []; } + +/** + * The link inputs for a Linux link without lld. The runtime's + * scr_glibc_math.c defines glibc's floor, fmod, frexp and a few others as + * hidden functions so that a program calling no other libm function starts + * without libm.so. That works with lld, which garbage-collects the unused + * definitions and decides `--as-needed` from the live code. GNU ld decides + * `--as-needed` before section GC, so the runtime's unreachable code keeps + * libm needed, and it keeps every regular definition of a name that a needed + * shared library also defines, hidden or not: the unit would add all of its + * code to every executable and save nothing. Its functions answer what + * glibc's do, so linking glibc's own instead is equivalent. + */ +export function inputsWithoutLld(platform: string, inputs: readonly string[]): string[] { + return platform === "linux" + ? inputs.filter((input) => basename(input) !== "scr_glibc_math.o") + : [...inputs]; +} diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index 2efeb96ac..d227ac005 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -141,7 +141,7 @@ const CYC_GEN = 3; /** ScrCycHdr.buf, an i32 at this header offset. */ export const CYC_BUF = 4; /** The emitted descriptor `{ trace, free_fn, kind, id }` (ScrCycType). */ -const CYC_TYPE = "{ ptr, ptr, i32, i32 }"; +export const CYC_TYPE = "{ ptr, ptr, i32, i32 }"; function declareState(host: AllocHost): void { if (host.threadLocalAlloc !== true) host.declare(`@scr_sa = external global ${SA_TYPE}`); diff --git a/packages/compiler/src/backend/llvm/arenas.test.ts b/packages/compiler/src/backend/llvm/arenas.test.ts new file mode 100644 index 000000000..1ab4b010a --- /dev/null +++ b/packages/compiler/src/backend/llvm/arenas.test.ts @@ -0,0 +1,114 @@ +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 "../../index.js"; + +async function llvmFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-arenas-")); + try { + const entry = join(dir, "main.ts"), + output = join(dir, "main.ll"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "llvm" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + return await readFile(output, "utf8"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The bodies of the module's arena-aware constructors. */ +function arenaConstructors(llvm: string): string[] { + return [...llvm.matchAll(/^define internal ptr @[^(]*\.r\(\)[^\n]*\n[\s\S]*?^}/gm)].map( + (m) => m[0], + ); +} + +const TREE = ` +class Node { + left: Node | null; + right: Node | null; + constructor(left: Node | null, right: Node | null) { + this.left = left; + this.right = right; + } + count(): number { + return 1 + (this.left === null ? 0 : this.left.count()) + (this.right === null ? 0 : this.right.count()); + } +} +function tree(depth: number): Node { + return depth === 0 ? new Node(null, null) : new Node(tree(depth - 1), tree(depth - 1)); +} +interface Leaf { + left: Leaf | null; + right: Leaf | null; +} +function leaves(depth: number): Leaf { + return depth === 0 ? { left: null, right: null } : { left: leaves(depth - 1), right: leaves(depth - 1) }; +} +function size(leaf: Leaf): number { + return leaf.left === null || leaf.right === null ? 1 : 1 + size(leaf.left) + size(leaf.right); +} +`; + +test("when every arena is closed, arena-aware constructors take no dispose list", async () => { + const llvm = await llvmFor(`${TREE} +let total = 0; +for (let d = 0; d < 4; d++) total += tree(d).count() + size(leaves(d)); +console.log(total); +`); + expect(llvm).toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 1\)/); + expect(llvm).not.toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 0\)/); + const constructors = arenaConstructors(llvm); + expect(constructors.length).toBe(2); + for (const body of constructors) { + expect(body).not.toContain("ScrArena.closed"); + expect(body).not.toContain("@scr_arena_disposal"); + } +}); + +test("an arena that is not closed keeps the dispose list in every arena-aware constructor", async () => { + // The rings hold strings and outside categories, so their groups are not closed. + const llvm = await llvmFor(`${TREE} +class Category { + name: string; + constructor(name: string) { + this.name = name; + } +} +class Member { + next: Member | undefined = undefined; + category: Category; + label: string; + constructor(category: Category, label: string) { + this.category = category; + this.label = label; + } +} +function ring(categories: Category[], size: number, seed: number): Member { + const first = new Member(categories[seed % categories.length]!, \`m\${seed}\`); + let last = first; + for (let i = 1; i < size; i++) { + const member = new Member(categories[(seed + i) % categories.length]!, \`m\${seed}-\${i}\`); + last.next = member; + last = member; + } + last.next = first; + return first; +} +const categories = [new Category("a"), new Category("b")]; +let total = 0; +for (let round = 0; round < 4; round++) { + const r = ring(categories, 30, round); + total += r.next!.label.length + r.category.name.length + tree(round).count(); +} +console.log(total); +`); + expect(llvm).toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 1\)/); + expect(llvm).toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 0\)/); + const withFields = arenaConstructors(llvm).filter((body) => body.includes(".dispose")); + expect(withFields.length).toBeGreaterThan(0); + for (const body of withFields) expect(body).toContain("ScrArena.closed"); +}); diff --git a/packages/compiler/src/backend/llvm/arenas.ts b/packages/compiler/src/backend/llvm/arenas.ts new file mode 100644 index 000000000..3e3954856 --- /dev/null +++ b/packages/compiler/src/backend/llvm/arenas.ts @@ -0,0 +1,442 @@ +/* Group arenas (runtime scr_arena.h): objects that region-groups.ts proves + * die together are bump-allocated in an arena and released at once. + * + * - A root (a fresh-result call or `new` whose value its caller keeps in + * one scope) opens an arena before it is evaluated and closes it after. + * The scope that holds the value drops the arena when it exits, on every + * path. A statement-frame entry closes the arena too, so an exception + * that skips the close never leaves it the allocation target. + * - A call region (a call of a leak-free function that returns no + * reference) opens an arena and drops it as soon as the call returns, + * or when its statement frame unwinds. + * + * region-sites.ts keeps only the roots and call regions whose groups grow, + * and picks the allocations that take the arena: those whose value the + * group keeps. Arena-aware functions are the ones reached from the kept + * roots and call regions. There, a kept `new`, record or array literal + * calls the arena-aware constructor (`.r`, `scr_arr_new*_r`), which + * allocates in the current arena when one is open and on the heap + * otherwise, and a call whose value the group does not keep runs with the + * arena suspended. Every other allocation site stays as it is. + * + * Arenas are off in programs that construct weak collections, in programs + * of more than ARENA_FUNCTION_LIMIT functions, in libraries, and with + * lifetime regions. */ +import type { IrClassDef, IrFunction, IrModule, IrType } from "../../ir/ir.js"; +import { + analyzeClosedGroups, + analyzeRegionGroups, + source, + type RegionGroups, +} from "./region-groups.js"; +import { CYC_BUF, CYC_HDR, CYC_TYPE } from "./alloc.js"; +import { ALLOCATIONS, children, forEachNode, isExpr, type Node } from "./region-ir.js"; +import { planRegionSites, reachableFrom, type RegionSites } from "./region-sites.js"; + +/** The count an arena object starts with (scr_arena.h SCR_ARENA_RC). */ +export const ARENA_RC = 2 ** 29; + +/** Group arenas are on unless SCRIPTC_ARENAS=0 (for comparisons). */ +export function arenasRequested(): boolean { + return process.env["SCRIPTC_ARENAS"] !== "0"; +} + +export interface ArenaPlan { + groups: RegionGroups; + sites: RegionSites; + /** Record shapes and classes that get an arena-aware constructor. */ + shapes: Set; + classes: Set; + /** Every root and call region opens a closed arena, so the current arena + * is always closed or absent, and the arena-aware constructors of record + * shapes and classes add no dispose entries. */ + allClosed: boolean; +} + +export interface ArenaEligibility { + shape(shapeId: string): boolean; + cls(def: IrClassDef): boolean; + /** The reference arm of a union stored as a nullable pointer, or null. */ + nullableArm(unionId: string): IrType | null; + /** Whether values of `t` can be on a reference cycle (the collector + * traces them). */ + traced(t: IrType): boolean; +} + +/** Whether `mod` can construct a WeakMap or WeakSet, directly or through + * a stored global constructor. A weak collection's entry for a key goes + * when the key is freed, and an arena object is never freed on its own, so + * such programs use no arenas. */ +function constructsWeakCollections(mod: IrModule): boolean { + let found = false; + const visit = (node: Node): void => { + if (found) return; + if (node.kind === "libCall") { + if (node.fn === "weakMap.new" || node.fn === "weakSet.new") found = true; + else if (node.fn === "dyn.globalConstructor") { + const name = node.args[0]; + if (name?.kind !== "strLit" || name.value === "WeakMap" || name.value === "WeakSet") + found = true; + } + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) visit(k); + }; + for (const fn of mod.functions) for (const s of fn.body) visit(s); + return found; +} + +/** Programs with more functions than this use no arenas: the analysis + * visits every function, and on programs this large it costs more compile + * time than their arenas save at run time. */ +const ARENA_FUNCTION_LIMIT = 4000; + +/** The arena plan of `mod`, or null when nothing would use an arena. */ +export function planArenas(mod: IrModule, eligible: ArenaEligibility): ArenaPlan | null { + if (mod.functions.length > ARENA_FUNCTION_LIMIT || constructsWeakCollections(mod)) return null; + const groups = analyzeRegionGroups(mod); + const sites = planRegionSites(mod, groups); + if (groups.roots.size === 0 && groups.calls.size === 0) return null; + const shapes = new Set(); + const classes = new Set(); + const defs = new Map((mod.classes ?? []).map((c) => [c.name, c])); + const note = (node: Node): void => { + const kept = sites.arenaSites.has(node) || groups.roots.has(node); + if ( + kept && + node.kind === "recordLit" && + node.type.kind === "record" && + eligible.shape(node.type.shapeId) + ) + shapes.add(node.type.shapeId); + if (kept && node.kind === "new") { + const def = defs.get(node.className); + if (def !== undefined && eligible.cls(def)) classes.add(node.className); + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) note(k); + }; + for (const fn of mod.functions) + if (groups.reachable.has(fn.name)) for (const s of fn.body) note(s); + for (const r of groups.roots) if (r.kind === "new") note(r); + // A group's heap objects die when its arena drops, unless heap objects + // hold each other in a cycle: the collector frees those later, and by + // then their references into the arena point at recycled memory. So a + // group keeps its arena only if every cycle-capable object it can + // allocate takes the arena. + const takesArena = (node: Node): boolean => + (sites.arenaSites.has(node) || groups.roots.has(node)) && + ((node.kind === "recordLit" && node.type.kind === "record" && shapes.has(node.type.shapeId)) || + (node.kind === "new" && classes.has(node.className)) || + node.kind === "arrayLit"); + const unsafe = cycleUnsafeFunctions(mod, groups.reachable, sites, takesArena, eligible.traced); + for (const r of [...groups.roots, ...groups.calls]) { + const runs = reachableFrom(mod, [r]); + if ([...runs].some((f) => unsafe.has(f)) || (r.kind === "new" && !takesArena(r))) { + groups.roots.delete(r); + groups.calls.delete(r); + groups.drops.delete(r); + } + } + if (groups.roots.size === 0 && groups.calls.size === 0) return null; + groups.reachable = reachableFrom(mod, [...groups.roots, ...groups.calls]); + const arenaType = (t: IrType): boolean => { + if (t.kind === "object") return classes.has(t.className); + if (t.kind === "record") return shapes.has(t.shapeId); + if (t.kind === "union") { + const arm = eligible.nullableArm(t.unionId); + return arm !== null && arenaType(arm); + } + // Arrays of arena objects: kept array literals of them take the arena. + return t.kind === "array" && t.elem.kind !== "union" && arenaType(t.elem); + }; + const keptSite = (e: Node): boolean => sites.arenaSites.has(e) || groups.roots.has(e); + for (const r of analyzeClosedGroups(mod, groups, arenaType, keptSite)) groups.closed.add(r); + const allClosed = [...groups.roots, ...groups.calls].every((r) => groups.closed.has(r)); + return { groups, sites, shapes, classes, allClosed }; +} + +/** The functions in `reachable` that can allocate a cycle-capable object + * on the heap while an arena is open: one that does not take the arena + * (`takesArena`), or a call that runs with the arena suspended and reaches + * any cycle-capable allocation. */ +function cycleUnsafeFunctions( + mod: IrModule, + reachable: ReadonlySet, + sites: RegionSites, + takesArena: (node: Node) => boolean, + traced: (t: IrType) => boolean, +): Set { + const fns = new Map(mod.functions.map((f) => [f.name, f])); + const allocatesTraced = (node: Node): boolean => + isExpr(node) && + traced(node.type) && + (node.kind === "closure" + ? node.captures.length > 0 + : (ALLOCATIONS.has(node.kind) || source(node) || node.kind === "libCall") && + node.kind !== "call" && + node.kind !== "callValue"); + // Functions with a cycle-capable allocation, and what they call. + const direct = new Set(); + const calls = new Map>(); + const heap = new Set(); + for (const name of reachable) { + const fn = fns.get(name); + if (fn === undefined) continue; + const out = new Set(); + calls.set(name, out); + forEachNode(fn.body, (node) => { + if (allocatesTraced(node)) { + direct.add(name); + if (!takesArena(node)) heap.add(name); + } + if (node.kind === "call") out.add(node.callee); + else if (node.kind === "new") out.add(`%${node.className}.constructor`); + else if (node.kind === "closure") out.add(node.fnName); + }); + } + const tracedSomewhere = new Set(direct); + for (let changed = true; changed;) { + changed = false; + for (const [name, out] of calls) + if (!tracedSomewhere.has(name) && [...out].some((c) => tracedSomewhere.has(c))) { + tracedSomewhere.add(name); + changed = true; + } + } + for (const name of reachable) { + const fn = fns.get(name); + if (fn === undefined || heap.has(name)) continue; + forEachNode(fn.body, (node) => { + if (!sites.heapCalls.has(node)) return; + const target = + node.kind === "call" + ? node.callee + : node.kind === "new" + ? `%${node.className}.constructor` + : null; + // A suspended call of an unknown function (a closure, a virtual call) + // may allocate anything. + if (target === null || tracedSomewhere.has(target)) heap.add(name); + }); + } + return heap; +} + +/** The arena-aware constructor of a record shape or class. */ +export function arenaNewSym(newSym: string): string { + return `${newSym}.r`; +} + +/** The function that releases an arena object's fields at the drop. */ +export function arenaDisposeSym(newSym: string): string { + return `${newSym}.dispose`; +} + +/** Whether `fn` allocates through arena-aware constructors. */ +export function arenaAware(plan: ArenaPlan | null, fn: IrFunction | undefined): boolean { + return plan !== null && fn !== undefined && plan.groups.reachable.has(fn.name); +} + +/** What arenaNewLines needs from its host (a subset of ShapeHost). */ +export interface ArenaHost { + declare(decl: string): void; + threadLocalAlloc?: boolean; + inlineAlloc?: boolean; + /** The target C ABI's `size_t`: also the pointer width. */ + readonly sizeType: "i32" | "i64"; +} + +/** Instructions that set `dest` to the address of the calling thread's + * open arena pointer (scr_arena.c's scr_arena_cur), or null on targets + * without the inline allocator, which call scr_arena_current, + * scr_arena_suspend and scr_arena_resume instead. In worker programs the + * pointer is thread-local. */ +export function arenaCurrentAddress(host: ArenaHost, dest: string): string[] | null { + if (host.inlineAlloc !== true) return null; + if (host.threadLocalAlloc !== true) { + host.declare(`@scr_arena_cur = external global ptr`); + return [`${dest} = getelementptr inbounds i8, ptr @scr_arena_cur, i64 0`]; + } + host.declare(`@scr_arena_cur = external thread_local global ptr`); + host.declare(`declare nonnull ptr @llvm.threadlocal.address.p0(ptr nonnull)`); + return [`${dest} = call ptr @llvm.threadlocal.address.p0(ptr @scr_arena_cur)`]; +} + +/** + * The arena-aware constructor `.r`: an object of `size` bytes (a + * `size_t` constant expression) in the current arena, behind the collector header + * `cyc` names (or none), which `init` sets up the way the heap constructor + * does (with the count ARENA_RC). When `dispose` names a function and the + * arena is not closed, the object goes on the arena's dispose list, which + * the drop runs. The heap constructor's object when no arena is open or + * the arena is full. The bump, the clear, the header and the dispose entry + * are inline (scr_arena.c's ScrArena ABI); scr_arena_obj takes a new chunk + * and scr_arena_disposal grows the list. + */ +export function arenaNewLines( + host: ArenaHost, + newSym: string, + size: string, + cyc: string | null, + dispose: string | null, + init: string[], + result: string, +): string[] { + const S = host.sizeType; + // ScrArena's fields and dispose entries are pointer-sized (scr_arena.c). + const word = S === "i32" ? 4 : 8; + host.declare(`declare ptr @scr_arena_obj(ptr, ${S}, ptr)`); + host.declare(`declare void @llvm.memset.p0.${S}(ptr, i8, ${S}, i1)`); + const cur = arenaCurrentAddress(host, "%cur"); + const current = + cur === null + ? (host.declare(`declare ptr @scr_arena_current()`), [` %a = call ptr @scr_arena_current()`]) + : [...cur.map((line) => ` ${line}`), ` %a = load ptr, ptr %cur`]; + const lines: string[] = [ + `define internal ptr @${arenaNewSym(newSym)}() nounwind alwaysinline { ; in the open arena`, + `entry:`, + ...current, + ` %on = icmp ne ptr %a, null`, + ` br i1 %on, label %arena, label %heap`, + `arena:`, + ]; + // A traced type's first object registers its descriptor on the slow path. + if (cyc !== null) + lines.push( + ` %tidp = getelementptr inbounds ${CYC_TYPE}, ptr ${cyc}, i32 0, i32 3`, + ` %tid = load atomic i32, ptr %tidp acquire, align 4`, + ` %creg = icmp ne i32 %tid, 0`, + ); + const hdr = cyc === null ? 0 : CYC_HDR; + lines.push( + ` %n0 = add ${S} ${size}, ${hdr + 7}`, + ` %n = and ${S} %n0, -8`, + ` %bp = load ptr, ptr %a`, + ` %limp = getelementptr inbounds i8, ptr %a, ${S} ${word}`, + ` %lim = load ptr, ptr %limp`, + ` %bpi = ptrtoint ptr %bp to ${S}`, + ` %limi = ptrtoint ptr %lim to ${S}`, + ` %room = sub ${S} %limi, %bpi`, + ` %fits0 = icmp ule ${S} %n, %room`, + ); + if (cyc !== null) lines.push(` %fits = and i1 %fits0, %creg`); + lines.push( + ` br i1 ${cyc === null ? "%fits0" : "%fits"}, label %carve, label %slow`, + `carve:`, + ` %end = getelementptr inbounds i8, ptr %bp, ${S} %n`, + ` store ptr %end, ptr %a`, + ` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`, + ); + if (cyc !== null) + lines.push( + ` %id = trunc i32 %tid to i16`, + ` store i16 %id, ptr %bp ; ScrCycHdr.id`, + ` %bufp = getelementptr inbounds i8, ptr %bp, i64 ${CYC_BUF}`, + ` store i32 -1, ptr %bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`, + ); + lines.push( + ` %of = getelementptr inbounds i8, ptr %bp, ${S} ${hdr}`, + ` br label %init`, + `slow:`, + ` %os = call ptr @scr_arena_obj(ptr %a, ${S} ${size}, ptr ${cyc ?? "null"})`, + ` %got = icmp ne ptr %os, null`, + ` br i1 %got, label %init, label %heap`, + `init:`, + ` %o = phi ptr [ %of, %carve ], [ %os, %slow ]`, + ...init, + ); + if (dispose !== null) { + host.declare(`declare void @scr_arena_disposal(ptr, ptr, ptr)`); + lines.push( + ` %closedp = getelementptr inbounds i8, ptr %a, ${S} ${4 * word} ; ScrArena.closed`, + ` %closed = load i32, ptr %closedp`, + ` %skip = icmp ne i32 %closed, 0`, + ` br i1 %skip, label %done, label %list`, + `list:`, + ` %dpp = getelementptr inbounds i8, ptr %a, ${S} ${2 * word} ; ScrArena.disposals`, + ` %dp = load ptr, ptr %dpp`, + ` %dep = getelementptr inbounds i8, ptr %a, ${S} ${3 * word} ; ScrArena.disposals_end`, + ` %de = load ptr, ptr %dep`, + ` %full = icmp eq ptr %dp, %de`, + ` br i1 %full, label %grow, label %push`, + `push:`, + ` store ptr %o, ptr %dp ; ScrArenaDisposal.obj`, + ` %dfp = getelementptr inbounds i8, ptr %dp, ${S} ${word}`, + ` store ptr @${dispose}, ptr %dfp ; ScrArenaDisposal.dispose`, + ` %dn = getelementptr inbounds i8, ptr %dp, ${S} ${2 * word}`, + ` store ptr %dn, ptr %dpp`, + ` br label %done`, + `grow:`, + ` call void @scr_arena_disposal(ptr %a, ptr %o, ptr @${dispose})`, + ` br label %done`, + `done:`, + ); + } + lines.push( + ` ret ptr ${result}`, + `heap:`, + ` %h = call ptr @${newSym}()`, + ` ret ptr %h`, + `}`, + ``, + ); + return lines; +} + +/** Dispose lines for field `i` of `%o` at `ptr` (a getelementptr into + * `%o`), released by `release`: a plain reference (no union encoding) is + * released only when it is a heap object, since an arena or immortal + * object's count (the rc word's low half) is at least 2^28 and its release + * would change nothing. */ +export function disposeFieldLines( + i: number, + gep: string, + plain: boolean, + release: string, + comment: string, +): string[] { + const load = [` %f${i} = ${gep}`, ` %v${i} = load ptr, ptr %f${i}`]; + if (!plain) return [...load, ` call void ${release}(ptr %v${i}) ; ${comment}`]; + return [ + ...load, + ` %nn${i} = icmp ne ptr %v${i}, null`, + ` br i1 %nn${i}, label %chk${i}, label %next${i}`, + `chk${i}:`, + ` %rc${i} = load i32, ptr %v${i}`, + ` %heap${i} = icmp ult i32 %rc${i}, ${2 ** 28}`, + ` br i1 %heap${i}, label %rel${i}, label %next${i}`, + `rel${i}:`, + ` call void ${release}(ptr %v${i}) ; ${comment}`, + ` br label %next${i}`, + `next${i}:`, + ]; +} + +/** Whether values of `t` are plain pointers to counted objects. */ +export function plainRefType(t: IrType): boolean { + return ( + t.kind === "object" || + t.kind === "record" || + t.kind === "array" || + t.kind === "map" || + t.kind === "set" || + t.kind === "func" || + t.kind === "string" + ); +} + +/** The dispose function: releases each reference field (`fields`, lines + * that load `%v` and release it) of the object `%o`. */ +export function arenaDisposeLines(newSym: string, fields: string[]): string[] { + return [ + `define internal void @${arenaDisposeSym(newSym)}(ptr %o) nounwind { ; arena drop`, + `entry:`, + ...fields, + ` ret void`, + `}`, + ``, + ]; +} diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index eee4d00b9..3ae65cb0f 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -103,8 +103,8 @@ export class BlockBuilder { } } - 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-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 0fa8fea34..76f229215 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -222,6 +222,55 @@ test("negative factors, rebinding, captures and outer loops are not versioned", expect(match(fn([outer]), inner)).not.toBeNull(); }); +test("loops with nested loops version only for accesses inside them", () => { + // for (let i = 0; i < 64; i++) { + // let t = 0; + // for (let j = 0; j < n; j++) t = t + src[j]; + // out[i] = t; + // } + const sum = (): IrStmt[] => [ + { kind: "varDecl", localId: "t", init: num(0), loc }, + counted( + [ + { + kind: "assign", + localId: "t", + value: bin("+", ref("t"), get(ref("src", f64s), ref("j"))), + loc, + }, + ], + ref("n"), + "j", + ), + ]; + const extra = [local("t", F64, true)]; + // The guard would prove only `out[i]`, which runs once per row. + const rows = counted([...sum(), set(ref("out", f64s), ref("i"), ref("t"))], num(64)); + expect(match(fn([rows], extra), rows)).toBeNull(); + expect(match(fn([rows], extra), rows.body[1] as IrStmt & { kind: "for" })).not.toBeNull(); + // The guard proves `out[i]` inside a nested loop too. + const inside = counted( + [...sum(), counted([set(ref("out", f64s), ref("i"), ref("t"))], num(2), "j")], + num(64), + ); + expect(match(fn([inside], extra), inside)?.bounds.size).toBe(1); + // A loop whose nested loops access no typed array versions for its own + // accesses. + const scalar = counted( + [ + { kind: "varDecl", localId: "t", init: num(0), loc }, + counted( + [{ kind: "assign", localId: "t", value: bin("+", ref("t"), ref("j")), loc }], + ref("n"), + "j", + ), + set(ref("out", f64s), ref("i"), ref("t")), + ], + num(64), + ); + expect(match(fn([scalar], extra), scalar)?.bounds.size).toBe(1); +}); + test("emitted loops version the body and tag typed-array memory", () => { const loop = counted([ set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))), @@ -320,16 +369,17 @@ test("worker programs test each receiver's shared storage once in the guard", () 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, and so - // does the copy for shared views whose bounds hold. + // 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).toContain("load atomic double"); - expect(shared).toContain("store atomic double"); + 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", () => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 6792ff73e..d0bef7fd6 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -15,9 +15,10 @@ import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety, - emitExactIntegerTest, 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 { InternalCompilerError } from "../../errors.js"; @@ -195,9 +196,10 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * and writes thread-owned views directly. * * Only small synchronous loops are versioned (nested loops included), and - * the emitter bounds how deeply versions nest. The checked loop runs - * whenever the guard fails, and keeps every out-of-bounds read, ignored - * write and NaN-index behavior. */ + * the emitter bounds how deeply versions nest. A loop whose nested loops + * access typed arrays versions only for an access inside them. The checked + * loop runs whenever the guard fails, and keeps every out-of-bounds read, + * ignored write and NaN-index behavior. */ export function matchBytesLoopVersion( loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, @@ -491,7 +493,15 @@ export function matchBytesLoopVersion( const pairedCounters = paired !== null && counted === null ? new Set([paired.low.localId, paired.high.localId]) : null; const pairedNodes = new Map(); + // Accesses inside the loop's nested loops, and inside its nested while + // loops. + const nested = new Set(); + const nestedWhile = new Set(); + let loops = 0, + whiles = 0; const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { + if (loops > 0) nested.add(node); + if (whiles > 0) nestedWhile.add(node); const ref = root(array); if (!ref) return; if (facts.workers && ref.type.kind === "bytes" && !facts.owned.has(node)) { @@ -602,7 +612,15 @@ export function matchBytesLoopVersion( access(s, s.arr, s.index, 1); scope(s, s.arr, true); } - return everyStmtChild(s, visitExpr, visitStmt); + const isLoop = + s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile"; + const isWhile = s.kind === "while" || s.kind === "doWhile"; + if (isLoop) loops++; + if (isWhile) whiles++; + const result = everyStmtChild(s, visitExpr, visitStmt); + if (isLoop) loops--; + if (isWhile) whiles--; + return result; }; loop.body.every(visitStmt); if (loop.kind === "while") visitExpr(loop.cond); @@ -713,6 +731,18 @@ export function matchBytesLoopVersion( used.add(receiver); for (const node of nodes) bounds.add(node); } + // A loop whose nested loops access typed arrays versions only when the + // guard proves the bounds of one of those accesses (or, in a worker + // program, the ownership of one inside a nested while loop, which never + // versions for ownership alone). Otherwise the guard covers accesses that + // run once per pass, while each copy of the body repeats every nested + // loop and each nested loop's own version. + if ( + nested.size > 0 && + ![...bounds].some((node) => nested.has(node)) && + ![...ownedNodes].some((node) => nestedWhile.has(node)) + ) + return null; const counterChecks: CounterCheck[] = []; for (const [id, min] of counterMins) { const info = whileAccesses!.counters.get(id)!; @@ -1228,7 +1258,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); @@ -1258,6 +1288,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(); @@ -1296,7 +1329,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}`); diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index f472eec60..e7ba72784 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -87,9 +87,13 @@ test("teardowns of types with children of their own type free their blocks as on 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, diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 74880453f..dd639393f 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -1,3 +1,11 @@ +import { + ARENA_RC, + arenaDisposeLines, + arenaDisposeSym, + arenaNewLines, + disposeFieldLines, + plainRefType, +} from "./arenas.js"; import { InternalCompilerError } from "../../errors.js"; /** Class layouts, reference counting, hierarchy intervals, virtual tables and * constructor values. Opaque LLVM pointers allow virtual slots to store method @@ -45,6 +53,7 @@ import { directDestroyTarget, segmentPrologueLines, segmentTeardownLines, + iterativeTeardownDefinitions, segmentedTeardown, usedDirectTargets, type DirectDestroy, @@ -58,6 +67,7 @@ import { traceAdapter, type ShapeHost, youngEntryLines, + arrElemInit, } from "./shapes.js"; /** One virtual method slot of a hierarchy: the ROOT-MOST declaring class @@ -371,11 +381,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. */ @@ -473,7 +506,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 @@ -733,7 +770,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) { @@ -908,13 +950,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(", ") : ""} } ` + @@ -1001,6 +1045,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); @@ -1017,6 +1062,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); @@ -1046,9 +1097,15 @@ export function emitClassShapes( // A traced class whose trace visits nothing carries the collector header // only because its hierarchy is traced: it is never on a cycle. Its // descriptor names scr_cyc_no_children and its objects start out - // SCR_CYC_ACYCLIC, so releases never buffer them. + // SCR_CYC_ACYCLIC, so releases never buffer them. An Array subclass's + // trace also visits its elements by their current element ops + // (scr_arr_subclass_trace), so it is never acyclic. const acyclic = - traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0; + traced && + runtimePrefix === null && + !isStreamRooted && + tracedFields.length === 0 && + cls.arrayElem === undefined; const bounded = runtimePrefix !== null || isStreamRooted || @@ -1098,6 +1155,7 @@ export function emitClassShapes( const destroy = `${meta.hierarchy ? mangleClassReleaseDirect(cls.name) : mangleClassRelease(cls.name)}_destroy`; const segmented = chained && + cls.arrayElem === undefined && !meta.compact && runtimePrefix === null && !isStreamRooted && @@ -1113,7 +1171,12 @@ export function emitClassShapes( // members). Runs at rc == 0. const teardown = (lines: string[]): void => { const release = (prefix: string, f: (typeof refFields)[number], t: number): string[] => { - const direct = bounded ? directDestroyTarget(host, f.type, directTargets) : null; + // 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)}`, @@ -1244,9 +1307,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, @@ -1258,6 +1369,18 @@ export function emitClassShapes( lifetime, chained, segmented, + false, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refFields.map((f) => f.index), + `@${mangleClassRelease(cls.name)}`, + "weak", + count, + ) + : null, ), ``, ); @@ -1306,6 +1429,8 @@ export function emitClassShapes( storedHeaderBytes(meta), ), ); + // The header and field set-up, shared with the arena-aware constructor. + const initStart = nw.length; if (meta.compact && !meta.hierarchy) nw.push(` store i32 ${compactHeaderWord(meta)}, ptr %o ; no count (compactHeaderWord)`); else if (meta.compact) { @@ -1331,6 +1456,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 @@ -1367,10 +1499,63 @@ 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)) + const init = nw.slice(initStart); + 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); + if ( + host.arenaClasses?.has(cls.name) === true && + cls.arrayElem === undefined && + !lifetime && + regions === null && + !meta.compact && + runtimePrefix === null && + !isStreamRooted && + !meta.threadSlot + ) { + // The arena count in place of 1, in the rc word or its low half. + const counted = meta.idInRc + ? ` store i64 ${classIdRcWord(meta, false)}, ptr %o.vt` + : ` store ${host.sizeType} 1, ptr %o`; + const arenaCount = meta.idInRc + ? ` store i64 ${(BigInt(meta.pre + 1) * 2n ** 32n + BigInt(ARENA_RC)).toString()}, ptr %o.vt` + : ` store ${host.sizeType} ${ARENA_RC}, ptr %o`; + const newSym = mangleClassNew(cls.name); + const dispose = + refFields.length > 0 && host.arenasClosed !== true ? arenaDisposeSym(newSym) : null; + defs.push( + ...arenaNewLines( + host, + newSym, + sizeOf, + traced ? cycType : null, + dispose, + init.map((line) => (line.startsWith(counted) ? arenaCount : line)), + meta.hierarchy ? "%o.vt" : "%o", + ), + ); + if (dispose !== null) + defs.push( + ...arenaDisposeLines( + newSym, + refFields.flatMap((f, t) => + disposeFieldLines( + t, + `getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, + plainRefType(f.type), + releaseSym(host, f.type), + llvmCommentText(f.name), + ), + ), + ), + ); + } if (traced) { // trace: visit exactly the cycle-capable fields; gcFree: release @@ -1390,6 +1575,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); @@ -1407,6 +1598,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); } @@ -1516,6 +1713,8 @@ export function emitClassObjDefs( `define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`, `entry:`, ` %o = call ptr @${mangleClassNew(className)}()`, + // Construction through a class value: the census counts it here. + ...(host.heapCensus?.newLines(className) ?? []), ...(meta.def.localCaptures !== undefined ? [ ` %class.owned = call ptr @scr_classobj_retain_v(ptr %class)`, diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index f1bdeb557..16ca07c65 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -32,6 +32,16 @@ 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; +/** The bits of ScrClosure.function_kind (SCR_CLOSURE_KIND_* in + * scr_closure.h; runtime-registries.test.ts checks they agree). */ +export const CLOSURE_KIND = { + GENERATOR: 1, + ASYNC: 2, + OWN_PROTOTYPE: 4, + CHECKED_ADAPTER: 8, + RECEIVER_FREE: 16, +} as const; + /** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a * size-type count. */ export function loadArrCount( @@ -154,6 +164,44 @@ export function exactIndexLines( return { lines, ok, wide }; } +/** exactIndexLines for the index `base + offset`, where `offset` is an + * integer of magnitude below 2^31: the round trip converts `base` instead of + * the sum, so accesses at several offsets from one base share the + * conversion. When `base` is an integer, the sum is in range exactly when + * the integer sum is (a length is at most 2^52, and a sum of 2^52 or more is + * out of range either way). A fractional `base` can still give an integer + * sum, as `1e-20 + 1` is 1, so `integral` false leaves the index to an + * exact check of the sum itself. */ +export function exactOffsetIndexLines( + base: string, + offset: number, + length: string, + sizeType: string, + tmp: () => string, +): { lines: string[]; ok: string; wide: string; integral: string } { + const raw = tmp(), + integer = tmp(), + back = tmp(), + integral = tmp(), + wide = tmp(), + below = tmp(), + ok = tmp(); + const lines = [ + `${raw} = fptosi double ${base} to i64`, + `${integer} = freeze i64 ${raw}`, + `${back} = sitofp i64 ${integer} to double`, + `${integral} = fcmp oeq double ${back}, ${base}`, + `${wide} = add i64 ${integer}, ${offset}`, + ]; + let len64 = length; + if (sizeType !== "i64") { + len64 = tmp(); + lines.push(`${len64} = zext ${sizeType} ${length} to i64`); + } + lines.push(`${below} = icmp ult i64 ${wide}, ${len64}`, `${ok} = and i1 ${integral}, ${below}`); + return { lines, ok, wide, integral }; +} + /** User-controlled text embedded after an LLVM `;` comment marker. Preserve * ordinary output unchanged, but encode control and line-separator code * units so a property name can never inject a line or invalid source byte. */ @@ -277,10 +325,6 @@ 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-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 39f97717b..88af25e47 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -5,9 +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 @@ -69,7 +71,7 @@ export function emitCountedLoopSafety( if (loop.startGuarded) { const start = host.emitExpr(loop.start); const bound = loop.startBound; - safe = andSafe(host, safe, emitExactIntegerTest(host, start.name, -bound, bound).ok); + 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)); @@ -138,6 +140,11 @@ 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"; @@ -213,6 +220,8 @@ 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; @@ -227,11 +236,55 @@ export function emitStartIntegerTest( min: number, max: number, ): { ok: string; integer: string } { - const value = host.emitExpr(expr); + 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 emitExactIntegerTest(host, value.name, min, max); - const wide = widenInteger(host, integer); - if (integer.range.min >= min && integer.range.max <= max) return { ok: "true", integer: wide }; + 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(), diff --git a/packages/compiler/src/backend/llvm/cpu-dispatch.ts b/packages/compiler/src/backend/llvm/cpu-dispatch.ts index f24fc8674..ced356cca 100644 --- a/packages/compiler/src/backend/llvm/cpu-dispatch.ts +++ b/packages/compiler/src/backend/llvm/cpu-dispatch.ts @@ -18,10 +18,10 @@ const COPY_GROUPS: ReadonlyMap = new Map([ ["2", "4"], ]); -export function cpuDispatchAttributes(asan: string): string[] { +export function cpuDispatchAttributes(common: string): string[] { return [ - `attributes #3 = {${asan} "target-cpu"="x86-64-v3" }`, - `attributes #4 = { noinline${asan} "target-cpu"="x86-64-v3" }`, + `attributes #3 = {${common} "target-cpu"="x86-64-v3" }`, + `attributes #4 = { noinline${common} "target-cpu"="x86-64-v3" }`, ]; } diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index e002ecd8a..b1e65a23f 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -19,6 +19,7 @@ * length, so reads never reach the state check for it; a write past the * bound tests the flag and, without states, appends at the length. */ +import { InternalCompilerError } from "../../errors.js"; import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js"; import { f64Lit, @@ -300,6 +301,9 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string { return state; } +/** The ScrElemKind of double arrays (scr_array.h). */ +export const SCR_ELEM_F64 = 0; + /** The ScrElemKind of int32 arrays (scr_array.h). */ export const SCR_ELEM_I32 = 7; @@ -368,7 +372,9 @@ export function emitDenseArrayGet( /** emitDenseArrayGet, with the int32 of an int32 array's element: the * slot itself on the dense path, the runtime getter's double converted - * (exactly: the element is an int32) on the other. */ + * (exactly: the element is an int32) on the other. With `inexact`, the + * getter's double may be something other than an int32 (NaN from + * `scr_arr_get_number`): the read branches there when it is. */ export function emitDenseArrayRead( host: LlvmEmitterContext, array: string, @@ -379,6 +385,7 @@ export function emitDenseArrayRead( getter: string, retain: boolean, proven = false, + inexact?: string, ): { value: string; int32: string | null } { const B = host.B; const ty = valueType(acc); @@ -460,22 +467,68 @@ export function emitDenseArrayRead( : READ_FALLBACK; B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`); let slowInt: string | null = null; - if (fastInt !== null) { + let slowLabel = slow; + if (fastInt !== null && inexact !== undefined) { + // An int32 that is not -0, or the inexact branch. + host.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)"); + slowInt = B.tmp(); + const back = B.tmp(), + exact = B.tmp(), + bits = B.tmp(), + notNegativeZero = B.tmp(), + ok = B.tmp(); + B.line(`${slowInt} = call i32 @llvm.fptosi.sat.i32.f64(double ${slowValue})`); + B.line(`${back} = sitofp i32 ${slowInt} to double`); + B.line(`${exact} = fcmp oeq double ${back}, ${slowValue}`); + B.line(`${bits} = bitcast double ${slowValue} to i64`); + B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`); + B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`); + slowLabel = B.newLabel("arr.get.int32"); + B.condBr(ok, slowLabel, inexact); + B.startBlock(slowLabel); + } else if (fastInt !== null) { slowInt = B.tmp(); B.line(`${slowInt} = fptosi double ${slowValue} to i32`); } B.br(join); B.startBlock(join); const result = B.tmp(); - B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`); + B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slowLabel} ]`); let int32: string | null = null; if (fastInt !== null && slowInt !== null) { int32 = B.tmp(); - B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`); + B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slowLabel} ]`); } return { value: result, int32 }; } +/** emitDenseArrayNumber of an int32 array, with the element's int32: a + * read that answers NaN (or another value that is not an int32) branches + * to `inexact`. */ +export function emitDenseArrayNumberInt32( + host: LlvmEmitterContext, + array: string, + index: LlValue, + indexExpr: IrExpr | undefined, + proven: boolean, + inexact: string, +): { value: string; int32: string } { + const read = emitDenseArrayRead( + host, + array, + index, + indexExpr, + "i32", + { kind: "f64" }, + "scr_arr_get_number", + false, + proven, + inexact, + ); + if (read.int32 === null) throw new InternalCompilerError("llvm emitter bug: int32 read"); + return { value: read.value, int32: read.int32 }; +} + /** `scr_arr_get_number`: a numeric element, or NaN for every missing, * hole, or present-undefined slot (answered by the runtime). */ export function emitDenseArrayNumber( 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 8c3e5de80..5c5a61a42 100644 --- a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -3,27 +3,52 @@ import { expect, test } from "vitest"; import { DYN_HANDLE_KINDS } from "../../ir/ir.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 +// scr_dyn.h declares the ScrDynHandleTag values in two lists: SCR_DYNH_TAGS +// pins tags to explicit values, and SCR_DYNH_NEW_TAGS names tags added since +// the last pin, which the enum numbers after the largest pinned value. Both +// are sorted by name, so changes that add tags edit different lines. The // header itself fails to compile on a duplicate or out-of-range value; these // checks report the same problems by name and keep the backend's numbers in // step. const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8"); -const body = /#define SCR_DYNH_TAGS\(X\) \\\n([\s\S]*?)\n\n/.exec(header)![1]!; -const tags = [...body.matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map(([, name, value]) => ({ +const macro = (name: string, param: string) => + new RegExp(`#define ${name}\\(${param}\\)(?: \\\\\\n([\\s\\S]*?))?\\n\\n`).exec(header)![1] ?? ""; +const pinnedBody = macro("SCR_DYNH_TAGS", "X"); +const newBody = macro("SCR_DYNH_NEW_TAGS", "Y"); +const unassignedBody = macro("SCR_DYNH_UNASSIGNED", "Z"); +const pinned = [...pinnedBody.matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map( + ([, name, value]) => ({ + name: `SCR_DYNH_${name!}`, + value: Number(value), + }), +); +const lastPinned = Math.max(...pinned.map((tag) => tag.value)); +const added = [...newBody.matchAll(/^\s*Y\(([A-Z0-9_]+)\)/gm)].map(([, name], i) => ({ name: `SCR_DYNH_${name!}`, - value: Number(value), + value: lastPinned + 1 + i, })); +const tags = [...pinned, ...added]; +const unassigned = [...unassignedBody.matchAll(/^\s*Z\((\d+)\)/gm)].map(([, value]) => + Number(value), +); const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]); +const rows = (body: string, open: string) => + body.split("\n").filter((line) => line.trim().startsWith(open)).length; -test("every ScrDynHandleTag has an explicit value, in name order", () => { - const lines = body.split("\n").filter((line) => line.trim().startsWith("X(")); - expect(tags.length).toBe(lines.length); - expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted()); +test("every pinned ScrDynHandleTag has an explicit value, in name order", () => { + expect(pinned.length).toBeGreaterThan(0); + expect(pinned.length).toBe(rows(pinnedBody, "X(")); + expect(pinned.map((tag) => tag.name)).toEqual(pinned.map((tag) => tag.name).toSorted()); +}); + +test("the new tags are listed by name only, in name order", () => { + expect(added.length).toBe(rows(newBody, "Y(")); + expect(added.map((tag) => tag.name)).toEqual(added.map((tag) => tag.name).toSorted()); }); test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => { - const owners = new Map(); + const owners = new Map(unassigned.map((value) => [value, "SCR_DYNH_UNASSIGNED"])); + expect(owners.size).toBe(unassigned.length); for (const { name, value } of tags) { expect(owners.get(value), `${name} = ${value}`).toBeUndefined(); owners.set(value, name); @@ -31,17 +56,27 @@ test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => { } }); -test("the backend's handle tag numbers match scr_dyn.h", () => { - const values = new Map(tags.map((tag) => [tag.name, tag.value])); +test("every value up to the largest pinned one is a tag or listed as unassigned", () => { + const used = new Set([...pinned.map((tag) => tag.value), ...unassigned]); + const gaps = Array.from({ length: lastPinned + 1 }, (_, value) => value).filter( + (value) => !used.has(value), + ); + expect(gaps, "add each value to SCR_DYNH_UNASSIGNED with the reason").toEqual([]); + for (const value of unassigned) expect(value).toBeLessThan(lastPinned); +}); + +test("the backend's handle tag numbers match pinned tags in scr_dyn.h", () => { + const values = new Map(pinned.map((tag) => [tag.name, tag.value])); for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) { const tag = DYN_HANDLE_KINDS.get(kind)?.tag; expect(tag, kind).toBeDefined(); - expect(values.get(tag!), `${kind} (${tag})`).toBe(num); + expect(values.get(tag!), `${kind} (${tag}) must be pinned with this value`).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])); +test("the backend's codec handle tag numbers match pinned tags in scr_dyn.h", () => { + const values = new Map(pinned.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); + expect(values.get("SCR_DYNH_STRING_DECODER")).toBe(TEXT_CODEC_TAG_NUM.StringDecoder); }); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 12ea99688..762bc52f4 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1,7 +1,14 @@ import { emitPerKeyOrder } from "./key-orders.js"; -import { typedRefConstructor } from "./shapes.js"; +import { rcMembers, typedRefConstructor } from "./shapes.js"; import { bindLiveArrayOps } from "./expr-stream-bridges.js"; -import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js"; +import { + BYTES_ELEM_NUM, + CLOSURE_KIND, + SCR_ARR_FIELD, + SCR_ARR_STATES, + f64Lit, + sizeToDouble, +} from "./common.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { INLINE_UNION_FIELD, @@ -25,10 +32,9 @@ import { InternalCompilerError } from "../../errors.js"; * are selected from the target C ABI. * v.num double | v.b i8 | v.str/v.bytes ptr at +16; * v.arr { len +16, cap +24, items +32 }; - * v.obj { len +16, cap +24, entries +32 }; + * v.obj { len +16 } (own properties; read them through + * scr_dyn_member_*, which know both object modes); * v.fn { clo +16, thunk +24, sig +32, name +40, arity +48 }. - * ScrDynEntry { key, key_len, value, getter, setter, accessor, writable, - * enumerable, configurable } — 48 bytes on 64-bit, 24 on 32-bit. * ScrDynKind: NULL=0 BOOL=1 NUM=2 STR=3 ARR=4 OBJ=5 UNDEF=6 BYTES=7 * FUNC=8 HANDLE=9. * ScrBytes { rc +0; len +8; elem +16; data +24 }. @@ -39,6 +45,7 @@ import { DYN_HANDLE_KINDS, isDirentShape, KEY_ORDER_FIELD, + shapeHasAccessorSlots, shapeKeyOrders, isDynTypedRefType, isRefCounted, @@ -145,8 +152,8 @@ const DYN_HANDLE_UNBOX_FN: Record = { dgramSocket: "scr_dgram_from_dyn", }; -/** ScrMessageKind (scr_message.h): the node kinds message readers test. */ -const MESSAGE_KIND = { +/** ScrMessageKind (scr_dyn.h): the node kinds message readers test. */ +export const MESSAGE_KIND = { UNDEFINED: 0, NULL: 1, BOOL: 2, @@ -254,6 +261,8 @@ export class LlDyn { /** Emitted function definitions, in interning order. */ readonly defs: string[] = []; + private readonly prefixSources = new Map(); + constructor(private readonly host: DynHost) {} /** Journal replay (emit-journal.ts): builds or finds the helper a chunk @@ -413,7 +422,7 @@ export class LlDyn { * dyn view: inside a visit session (`wrapped`), a capsule keeps the view * it was first given, so an outer visit can keep reading it. */ private viewFn(wrapped: boolean): string { - const fn = wrapped ? "scr_dyn_visit_view" : "scr_dyn_typed_ref_materialize"; + const fn = wrapped ? "scr_dyn_visit_view" : "scr_dyn_typed_ref_peek"; this.host.declare(`declare ptr @${fn}(ptr)`); return fn; } @@ -842,27 +851,23 @@ export class LlDyn { return e; } - /** Object entry field addresses: entries + i*sizeof(ScrDynEntry), with - * key, key_len, and value at offsets 0, 8, and 16 on 64-bit targets. */ + /** Own property i of object `d` (borrowed key bytes, key length and + * value), read through the runtime's accessors: the object may be in + * layout or dictionary mode (scr_dyn.h). */ private entryAt( B: BlockBuilder, - entries: string, + d: string, i: string, ): { key: string; keyLen: string; value: string } { - const off = B.tmp(); - const base = B.tmp(); - B.line(`${off} = mul ${this.S} ${i}, ${this.abiOffset(48, 24)} ; sizeof(ScrDynEntry)`); - B.line(`${base} = getelementptr inbounds i8, ptr ${entries}, ${this.S} ${off}`); + this.host.declare(`declare ptr @scr_dyn_member_key(ptr, ${this.S})`); + this.host.declare(`declare ${this.S} @scr_dyn_member_key_len(ptr, ${this.S})`); + this.host.declare(`declare ptr @scr_dyn_member_value(ptr, ${this.S})`); const key = B.tmp(); - B.line(`${key} = load ptr, ptr ${base}`); - const klp = B.tmp(); const keyLen = B.tmp(); - B.line(`${klp} = getelementptr inbounds i8, ptr ${base}, i64 ${this.abiOffset(8, 4)}`); - B.line(`${keyLen} = load ${this.S}, ptr ${klp}`); - const vp = B.tmp(); const value = B.tmp(); - B.line(`${vp} = getelementptr inbounds i8, ptr ${base}, i64 ${this.abiOffset(16, 8)}`); - B.line(`${value} = load ptr, ptr ${vp}`); + B.line(`${key} = call ptr @scr_dyn_member_key(ptr ${d}, ${this.S} ${i})`); + B.line(`${keyLen} = call ${this.S} @scr_dyn_member_key_len(ptr ${d}, ${this.S} ${i})`); + B.line(`${value} = call ptr @scr_dyn_member_value(ptr ${d}, ${this.S} ${i})`); return { key, keyLen, value }; } @@ -1148,10 +1153,63 @@ export class LlDyn { * Class layouts share their base prefix, so unboxing retains the same * pointer and virtual dispatch still uses the object's original vtable. */ private typedRefMatches(B: BlockBuilder, t: IrType): string { + return this.typedRefMatchesAny( + B, + isDynTypedRefType(t) + ? this.host.classSubtypes(t.className).map((className) => ({ kind: "object", className })) + : [t], + ); + } + + /** The record shapes whose values a pointer of shape `targetId` may + * reference unchanged: their struct begins with exactly the target's + * fields (same names, types and representations, so the same offsets), + * and everything the target's release, teardown and collector support + * would get wrong on them is absent. The extra fields hold no + * references (the target's release frees the block without visiting + * them; frees are size-class agnostic), neither shape carries a shape + * word, a cycle header, an overflow map, key order variants or accessor + * slots, and both freeze alike. A record that keeps undeclared source + * properties is refused at run time (scr_dyn_typed_ref_prefix_view). */ + private prefixViewSources(targetId: string): string[] { + const memo = this.prefixSources.get(targetId); + if (memo) return memo; + const host = this.host; + const plain = (shape: IrRecordShape): boolean => + !shape.tuple && + !shape.indexValue && + !isDirentShape(shape) && + !shapeHasAccessorSlots(shape) && + shapeKeyOrders(shape).length === 1 && + !shape.fields.some((field) => field.name === KEY_ORDER_FIELD) && + host.nullableUnions.shapeNumber(shape.id) === 0 && + !host.tracedShapes.has(`record:${shape.id}`); + const target = host.recordsById.get(targetId); + const out: string[] = []; + if (target && plain(target) && target.fields.length > 0) { + const base = host.nullableUnions.fieldBase(targetId); + for (const shape of host.recordsById.values()) { + if (shape.id === targetId || shape.fields.length <= target.fields.length) continue; + if (!plain(shape) || shape.freezable !== target.freezable) continue; + if (host.nullableUnions.fieldBase(shape.id) !== base) continue; + const prefix = target.fields.every( + (field, i) => + shape.fields[i]!.name === field.name && + typeKey(shape.fields[i]!.type) === typeKey(field.type), + ); + if (!prefix) continue; + const extra = rcMembers(shape, host.inlineUnions, base).some( + (member) => member.index >= target.fields.length + base && isRefCounted(member.type), + ); + if (!extra) out.push(shape.id); + } + } + this.prefixSources.set(targetId, out); + return out; + } + + private typedRefMatchesAny(B: BlockBuilder, types: readonly IrType[]): string { this.host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${this.S})`); - const types: IrType[] = isDynTypedRefType(t) - ? this.host.classSubtypes(t.className).map((className) => ({ kind: "object", className })) - : [t]; let matched = "false"; for (const type of types) { const key = typeKey(type); @@ -1434,9 +1492,8 @@ export class LlDyn { if (shape.indexValue && shape.indexValue.kind !== "dyn") { this.host.declare(`declare i32 @memcmp(ptr, ptr, ${this.S})`); const n = this.lenOf(B, "%d"); - const entries = this.itemsOf(B, "%d"); this.i64Loop(B, "dm.ov", n, (i, brNext) => { - const ent = this.entryAt(B, entries, i); + const ent = this.entryAt(B, "%d", i); // Skip declared keys. for (const f of shape.fields) { const klen = Buffer.byteLength(f.name, "utf8"); @@ -2165,6 +2222,32 @@ export class LlDyn { B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`); B.terminate(`ret ptr ${ref}`); B.startBlock(lNext); + if (t.kind === "record") { + // A record whose storage begins with exactly this type's storage + // is this type's value too: the conversion passes the pointer, so + // both names keep one object (decision D3). + const sources = this.prefixViewSources(t.shapeId); + if (sources.length > 0) { + const viewed = this.typedRefMatchesAny( + B, + sources.map((shapeId) => ({ kind: "record", shapeId })), + ); + const lView = B.newLabel("dc.prefix"); + const lOther = B.newLabel("dc.noprefix"); + B.condBr(viewed, lView, lOther); + B.startBlock(lView); + host.declare(`declare ptr @scr_dyn_typed_ref_prefix_view(ptr)`); + const view = B.tmp(); + B.line(`${view} = call ptr @scr_dyn_typed_ref_prefix_view(ptr %d)`); + const passed = B.tmp(); + B.line(`${passed} = icmp ne ptr ${view}, null`); + const lPass = B.newLabel("dc.prefix.pass"); + B.condBr(passed, lPass, lOther); + B.startBlock(lPass); + B.terminate(`ret ptr ${view}`); + B.startBlock(lOther); + } + } // A constructor-function stream (scr_dyn_builtin_stream.c) carries // its runtime stream. const streamKind = t.kind === "object" ? builtinStreamKind(t.className) : null; @@ -2770,9 +2853,8 @@ export class LlDyn { B.line(`store i8 ${np}, ptr ${flagsp}`); } const n = this.lenOf(B, "%d"); - const entries = this.itemsOf(B, "%d"); this.i64Loop(B, "dcv", n, (i, brNext) => { - const ent = this.entryAt(B, entries, i); + const ent = this.entryAt(B, "%d", i); for (const f of shape.fields) { const klen = Buffer.byteLength(f.name, "utf8"); const lenEq = B.tmp(); @@ -3174,7 +3256,7 @@ export class LlDyn { B.line(`${targetKind} = getelementptr inbounds %ScrClosure, ptr ${a}, i64 0, i32 4`); const adapterKind = B.tmp(); B.line( - `${adapterKind} = or i32 ${functionKind}, 8 ; preserves the checked callable in caps[0]`, + `${adapterKind} = or i32 ${functionKind}, ${CLOSURE_KIND.CHECKED_ADAPTER} ; preserves the checked callable in caps[0]`, ); B.line(`store i32 ${adapterKind}, ptr ${targetKind}`); const box = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 8c9ace9f3..427e439ad 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -31,6 +31,7 @@ import { findLocalStackUnions } from "./stack-unions.js"; import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js"; import { findLocalUnionStorage } from "./local-union-storage.js"; import { INT32_RANGE, analyzeIntegerRangeFacts } from "../../ir/integer-ranges.js"; +import { beginLoopEntryVersions } from "./loop-entry-versions.js"; import { findBytesBounds } from "./bytes-bounds.js"; import { findInt32Locals } from "./int32-locals.js"; import { findIntegerViews } from "./integer-views.js"; @@ -83,6 +84,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 @@ -158,6 +160,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(); @@ -244,6 +247,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const rangeFacts = analyzeIntegerRangeFacts(numericFn, slotFacts); emitter.integerRanges = rangeFacts.ranges; emitter.integerLoopRanges = rangeFacts.loopRanges; + beginLoopEntryVersions(emitter, numericFn, slotFacts, rangeFacts); emitter.int32Elements = slotFacts.elements ?? null; emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn); emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); @@ -336,6 +340,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { } emitter.currentCoro = { kind: fn.generator !== undefined ? "generator" : "async", + awaits: fn.async === true, id, handle, self, @@ -356,6 +361,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) { @@ -514,6 +528,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { } } emitter.scopes.push(fnScope); + emitter.beginArenaFunction(fn); // Stackful native fibers and native coroutine frames retain these frames // while suspended. The active exception context owns the chain, so // concurrent fibers isolate it. @@ -851,10 +866,14 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] { ]; const genNew = coroBody && !emitter.wasi ? "scr_gen_new_stackless" : "scr_gen_new"; if (genNew !== "scr_gen_new") emitter.declare(`declare ptr @${genNew}(ptr, ptr, ptr)`); + const asyncGenNew = + coroBody && !emitter.wasi ? "scr_async_gen_new_stackless" : "scr_async_gen_new"; + if (settleAsync !== null && asyncGenNew !== "scr_async_gen_new") + emitter.declare(`declare ptr @${asyncGenNew}(ptr, ptr, ptr, ptr)`); sp.push( settleAsync === null ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` - : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + : ` %gg = call ptr @${asyncGenNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, ...(fn.generator.tag !== undefined ? [` call void @scr_gen_set_tag(ptr %gg, ptr ${emitter.cstr(fn.generator.tag)})`] : []), diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index e56c03555..bb4d2421d 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"; @@ -67,6 +68,7 @@ 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 @@ -197,19 +199,22 @@ 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). */ + * the build's `--target-cpu` level, `sanitize_address` in sanitized builds, + * and `uwtable` on native targets, so every function has an unwind table + * entry, including those LLVM infers `nounwind` (profilers and debuggers + * unwind through them). A group needs one attribute: without any, #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"`; + const common = `${asan}${emitter.wasi ? "" : " uwtable"}`; + const base = `${common}${cpu}` || ` "scriptc-function"`; return [ `attributes #0 = {${base} }`, - ...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []), - ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []), - ...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []), + ...(emitter.usesCoroutines ? [`attributes #1 = {${common} presplitcoroutine${cpu} }`] : []), + ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${common}${cpu} }`] : []), + ...(emitter.cpuDispatch ? cpuDispatchAttributes(common) : []), ]; } @@ -233,6 +238,61 @@ function partitionGroupLines(functions: readonly IrFunction[]): string[] { /** Ends the group of the definitions that follow. */ const PARTITION_GROUP_END = "; scriptc-group"; +/** Writes the generated helpers, then the function definitions with their + * group lines. With a layout, the definitions it lists, helpers included, + * move after the unlisted helpers and before the other functions, by tier, + * each in its tier's group (`layout-`). 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. @@ -948,21 +1008,24 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ); } if (globals.length > 0) out.push(``); - for (const line of helpers) out.push(line); - for (const line of ffiCallbacks.defs) out.push(line); - for (const line of shapes.defs) out.push(line); - for (const line of classShapes.defs) out.push(line); - for (const line of classObjDefs) out.push(line); - for (const line of emitter.walkers.defs) out.push(line); - for (const line of emitter.dyn.defs) out.push(line); - for (const line of wrappers) out.push(line); - for (const line of asyncDefs) out.push(line); - for (const line of emitter.resolveThunkDefs) out.push(line); - const groups = partitionGroupLines(emitter.mod.functions); - for (let i = 0; i < fnDefs.length; i++) { - if (i > 0) out.push(""); - out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!); - } + writeDefinitions( + out, + emitter.targetOptions.functionLayout, + [ + helpers, + ffiCallbacks.defs, + shapes.defs, + classShapes.defs, + classObjDefs, + emitter.walkers.defs, + emitter.dyn.defs, + wrappers, + asyncDefs, + emitter.resolveThunkDefs, + ], + fnDefs, + partitionGroupLines(emitter.mod.functions), + ); if (fnDefs.length === 0) out.push(""); out.push(PARTITION_GROUP_END, ""); // WebAssembly modules translated into this program, and the registry. @@ -1040,6 +1103,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { 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; } @@ -1215,6 +1279,7 @@ 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; } diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index cc0af8ebb..7732fb8be 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, @@ -44,6 +46,7 @@ import { emitBorrowedInput } from "./borrowed-inputs.js"; import { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js"; import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js"; import { withinInt32 } from "../../ir/integer-ranges.js"; +import { emitLoopEntryVersion, loopEntryVersion } from "./loop-entry-versions.js"; import { resultSlotStore } from "../result-slots.js"; import { matchIntegerArrayForLoop, @@ -81,6 +84,7 @@ import { planProvenIntegerLoop, planSpeculativeLoop, type ProvenLoopPlan, + type SpeculativeLoopPlan, speculativeReads, speculativeWriteTarget, } from "./speculative-integers.js"; @@ -92,7 +96,9 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { LOOPS } from "./loop-state.js"; -import { loopAccessesTypedArrays } from "../loop-polls.js"; +import { loopUnrollAttachment, periodicGuardPeriod } from "./periodic-guards.js"; +import { containsLoop, 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; @@ -100,8 +106,11 @@ const MAX_CALLBACK_VERSION_COST = 400; export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const B = emitter.B; emitter.frames.push([]); + if (emitter.arenas !== null) + emitter.statementFrames.add(emitter.frames[emitter.frames.length - 1]!); switch (s.kind) { case "varDecl": { + emitter.ownArenasOf(s.localId); if (s.init && emitter.int32Locals.has(s.localId)) { emitter.storeInt32Local(s.localId, emitter.emitExpr(s.init), s.init); break; @@ -576,6 +585,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") @@ -607,6 +617,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { // (The guard precedes the value here: the nullable store // evaluates it itself.) emitter.emitStoreGuard(obj.name, s); + emitter.heapCensus?.countStore( + (line) => B.line(line), + () => B.tmp(), + s.className, + s.field, + s.value, + null, + ); emitter.emitNullableFieldStore(obj.name, s.className, s.field, nullable, s.value); break; } @@ -626,8 +644,17 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { ? emitter.recordFieldInline(s.shapeId, s.field) : emitter.inlineUnions.classField(s.className, s.field), ); - if (s.kind === "fieldSet") emitter.emitStoreGuard(obj.name, s); - else if (emitter.publishedTypes?.recordGuarded(s.shapeId) === true) + if (s.kind === "fieldSet") { + emitter.emitStoreGuard(obj.name, s); + emitter.heapCensus?.countStore( + (line) => B.line(line), + () => B.tmp(), + s.className, + s.field, + s.value, + v.name, + ); + } else if (emitter.publishedTypes?.recordGuarded(s.shapeId) === true) emitter.emitPublishedFieldGuard(obj.name, null, s.field); const { ptr, type, inline } = s.kind === "fieldSet" @@ -837,6 +864,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"); @@ -1215,8 +1249,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { }; // 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 + // of 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. // @@ -1265,6 +1299,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { if (stripMined) { const counter: StripMinedCounter = countedLoop ? { + localId: countedLoop.localId, slot: integerSlot!, type: "i64", signed: true, @@ -1276,6 +1311,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { limit: () => countedLimit!, } : { + localId: integerLoop!.localId, slot: integerSlot!, type: emitter.sizeType, signed: false, @@ -1301,6 +1337,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { 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); @@ -1361,7 +1404,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); @@ -1394,7 +1437,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); @@ -1648,7 +1691,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`call void @scr_async_gen_return_awaits()`); } if (emitter.currentAsyncGenerator && s.awaited !== true && emitter.finallyStack.length > 0) { - if (emitter.wasi) emitter.emitWasiSuspend(null); + if (emitter.coroAwait) emitter.emitCoroAwait(null); else { emitter.declare(`declare void @scr_await_hop()`); B.line(`call void @scr_await_hop()`); @@ -2324,6 +2367,13 @@ export function emitLoopCopies( exit: string, emitCopy: (exit: string) => void, ): void { + const entry = allowed ? loopEntryVersion(emitter, loop) : null; + if (entry) { + emitLoopEntryVersion(emitter, loop, entry, () => + emitLoopCopies(emitter, loop, allowed, exit, emitCopy), + ); + return; + } const plan = allowed && emitter.speculativeLoopsEnabled && @@ -2371,6 +2421,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"; @@ -2705,10 +2771,11 @@ function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number { return trips * (1 + nested); } -/** The counter of a strip-mined loop: 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. */ +/** 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; @@ -2721,8 +2788,8 @@ interface StripMinedCounter { * 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. + * follows the block. It polls once per block, 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 @@ -2800,7 +2867,7 @@ function emitStripMinedCountedLoop( const after = runSlot === null ? poll : B.newLabel("loop.end"); B.condBr(inBlock, lb, after); B.startBlock(lb); - emitter.jumpTargets.push({ + const target: LlEmitter["jumpTargets"][number] = { kind: "loop", brkLabel: exit, contLabel: lu, @@ -2808,8 +2875,19 @@ function emitStripMinedCountedLoop( frameDepth: emitter.frames.length, scopeDepth: emitter.scopes.length, finallyDepth: emitter.finallyStack.length, - }); - emitter.emitBlock(s.body); + }; + 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); @@ -2845,21 +2923,6 @@ function emitStripMinedCountedLoop( 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; diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 880685bf2..8e3aae24d 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -206,7 +206,7 @@ test("a target CPU level reaches every function attribute group and nothing else 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).toMatch(/^attributes #0 = \{ sanitize_address uwtable "target-cpu"="x86-64-v3" \}$/m); expect(v3.replaceAll(' "target-cpu"="x86-64-v3"', "")).toBe(plain); expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain); }); @@ -215,13 +215,21 @@ 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); + expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address uwtable \}$/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); + expect(plain).toMatch(/^attributes #0 = \{ uwtable \}$/m); const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" }); - expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m); + expect(v3).toMatch(/^attributes #0 = \{ uwtable "target-cpu"="x86-64-v3" \}$/m); +}); + +test("native functions get unwind tables and wasm32-wasi functions do not", () => { + const module = moduleFor([call()]); + expect(emitLlvmModule(module)).toMatch(/^attributes #0 = \{ sanitize_address uwtable \}$/m); + const wasi = emitLlvmModule(module, { wasi: true, pointerBits: 32, sanitizeAttributes: false }); + expect(wasi).not.toContain("uwtable"); + // A group needs an attribute: an ignored string attribute stands in. + expect(wasi).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m); }); test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b00eaa945..e6c1b98c5 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,4 +1,6 @@ import type { TargetCpu } from "../target-cpu.js"; +import { loopEntryInlines } from "./loop-entry-versions.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"; @@ -26,7 +28,7 @@ import { streamTypedRefEligible, undefinedArmTag, } from "../../ir/analysis.js"; -import { loopTestsEveryIteration } from "../loop-polls.js"; +import { containsLoop, loopTestsEveryIteration } from "../loop-polls.js"; import type { IrBytesElem, IrExpr, @@ -63,6 +65,7 @@ import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion } from "./bytes-loop-versions.js"; import { globalSlotAlias } from "./bytes-alias.js"; +import type { BytesHeaderScope } from "./bytes-headers.js"; import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import { materializeSplitPiece, @@ -127,6 +130,8 @@ import { } from "./int32-abi.js"; import { type Int32Local } from "./int32-locals.js"; import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js"; +import { writeRepresentationReport } from "./repr-report.js"; +import { heapCensusOf, type HeapCensus } from "./heap-census.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"; @@ -215,7 +220,7 @@ import { import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; import { - CLOSURE_RECEIVER_FREE, + CLOSURE_KIND, emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, @@ -341,6 +346,15 @@ import { type LlClassMeta, } from "./classes.js"; import { LifetimeRegions, REGION_TYPES } from "./regions.js"; +import { + arenaAware, + arenaCurrentAddress, + arenaNewSym, + arenasRequested, + planArenas, + type ArenaPlan, +} from "./arenas.js"; +import { children, type Node } from "./region-ir.js"; import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js"; import { VirtualBorrows } from "./virtual-borrows.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -373,6 +387,7 @@ import { retainSym, traceArg, vAdapters, + traceAdapter, } from "./shapes.js"; import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js"; import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js"; @@ -419,6 +434,7 @@ import { fieldAliasMetadata, isStackUnionSource, } from "./receivers.js"; +import { findLoopCallees } from "./loop-callees.js"; export { LlvmUnsupportedError } from "./unsupported.js"; @@ -463,6 +479,9 @@ export interface LlScopeEntry { slot: string; type: IrType; boxed?: boolean; + /** A group arena list slot (arenas.ts), dropped after the scope's other + * entries. */ + arena?: boolean; /** A lazily created box: the slot stays null on paths that build no * environment, which skip the release call. */ lazy?: boolean; @@ -499,6 +518,8 @@ export interface LlvmTargetOptions { /** 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. */ @@ -775,7 +796,9 @@ 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(); + const parts = emitter.emitParts(); + writeRepresentationReport(emitter); + return parts; } catch (error) { if (!(error instanceof UnwindAbiViolation)) throw error; const names = new Set(excluded); @@ -822,6 +845,11 @@ 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"]; + +/** workerLoopBudget's answer for a loop that polls on every iteration + * instead of counting down in a slot. */ +const POLL_EVERY_ITERATION = "%worker.poll.every"; + /** Runtime library calls (readsLoopMetrics). */ const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"]; @@ -836,16 +864,23 @@ function isIdentifierByte(code: number): boolean { } /** Runtime entry points (and await-call entries) that may park the running - * execution context, as they appear in emitted function text. */ + * execution context, as they appear in emitted function text. An async + * generator's next, return and throw only queue a request and run its body + * to its next suspension, which returns to the caller, and the body's own + * bookkeeping calls do not suspend it. */ export const SUSPENDS = - /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)|sc_a[wd]_)/; + /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_(?!new\b|next_|return_|throw\b|hop_done\b)|fiber_)|sc_a[wd]_)/; /** The suspensions of a fiber body's own awaits, which its stackless * coroutine form suspends itself at instead. */ -const AWAIT_SUSPENSIONS = /@(?:scr_await_(?:void|f64|bool|str|ref|hop)\b|sc_a[wd]_[A-Za-z0-9_]+)/g; +const AWAIT_SUSPENSIONS = + /@(?:scr_await_(?:void|f64|bool|str|ref|hop|dyn_value)\b|sc_a[wd]_[A-Za-z0-9_]+)/g; +/** The suspensions of a fiber generator body's own yields, which its + * coroutine form also suspends itself at. */ +const YIELD_SUSPENSIONS = /@scr_(?:gen_yield_(?:f64|bool|ref))\b/g; /** SUSPENDS in a stackless coroutine body, whose frame bookkeeping reads its * own fiber and promise. */ const COROUTINE_SUSPENDS = - /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/; + /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_(?!new\b|next_|return_|throw\b|hop_done\b)|fiber_(?!self\b|promise\b))|sc_a[wd]_)/; /** Captured bindings (boxes a body declares or receives) from which a * function compiles without LLVM optimization (see boxSlotCount). */ export const UNOPTIMIZED_BOX_SLOTS = 512; @@ -1267,11 +1302,13 @@ export class LlEmitter { * generator stacks) and so do debug builds (the debugger reads locals * from the native frame). */ private stacklessGenerators = false; - /** Async bodies that suspend only at their own awaits compile to the same - * switched coroutines on native targets (selectStacklessAsync), under the - * generators' conditions; library and Windows builds keep fibers. */ + /** Async bodies that suspend only at their own awaits (and an async + * generator's yields) compile to the same switched coroutines on native + * targets (selectStacklessAsync), under the generators' conditions; + * library and Windows builds keep fibers. */ private stacklessAsyncAllowed = false; - /** The async functions selectStacklessAsync lowered as coroutines. */ + /** The async functions and async generators selectStacklessAsync lowered + * as coroutines. */ readonly stacklessAsync = new Set(); /** Some function in the module is lowered as a coroutine. */ usesCoroutines = false; @@ -1480,6 +1517,9 @@ export class LlEmitter { /** Functions that check the native stack guard on entry * (stackCheckedFunctions); null where the runtime has no guard. */ readonly stackChecks: ReadonlySet | null; + /** Small leaf functions called only from loops, whose direct calls are + * `alwaysinline` (loop-callees.ts). */ + readonly loopCallees: ReadonlySet; /** Static types whose values can be published (@scriptc/threads): their * stores carry the frozen-object guard. Null without publish calls. */ readonly publishedTypes: PublishedTypes | null; @@ -1574,6 +1614,8 @@ export class LlEmitter { readonly inlineUnions: InlineUnionFields; /** Unions whose values are nullable arm pointers (nullable-unions.ts). */ readonly nullableUnions: NullableUnions; + /** The counters of a build instrumented for the heap census, or null. */ + readonly heapCensus: HeapCensus | null; /** The union table without nullable unions: the stack-box analyses. */ readonly boxedUnionsById: Map; readonly recordsById = new Map(); @@ -1666,6 +1708,8 @@ export class LlEmitter { * commit back into their objects. Library builds keep their commits: * their values cross a native boundary. */ readonly liveViewsReadOnly: boolean; + /** IrModule.sloppyDynWrites: untyped writes in sloppy-mode code. */ + readonly sloppyDynWrites: ReadonlySet; /** IrModule.foreignElementKeys: element keys whose arrays can hold * SCR_ARR_FOREIGN elements. */ readonly foreignElementKeys: ReadonlySet; @@ -1823,15 +1867,33 @@ export class LlEmitter { /** 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(); @@ -1876,9 +1938,12 @@ export class LlEmitter { currentUnionResult = false; /** Active only while emitting a body lowered with LLVM's * switched-coroutine intrinsics (wasm32 async bodies and generators, - * native synchronous generators; see coroLowered). */ + * native synchronous generators, and the native async bodies + * selectStacklessAsync chose; see coroLowered). */ currentCoro: { kind: "async" | "generator"; + /** The body awaits: an async function or an async generator. */ + awaits: boolean; id: string; handle: string; self: string; @@ -1943,6 +2008,8 @@ export class LlEmitter { options.debugSources === undefined, this.nodeIndex, ); + this.heapCensus = heapCensusOf(mod, this.nullableUnions); + for (const decl of this.heapCensus?.declarations() ?? []) this.declare(decl); this.mockTimers = this.runtimeFeatures.mockTimers; this.debug = options.debugSources === undefined @@ -1956,6 +2023,7 @@ export class LlEmitter { ); this.constantNumericTables = findConstantNumericTables(mod); this.untypedIntegrity = mod.untypedIntegrity === true; + this.sloppyDynWrites = new Set(mod.sloppyDynWrites ?? []); this.liveViewsReadOnly = mod.lib === undefined && !this.untypedIntegrity && @@ -2049,6 +2117,7 @@ export class LlEmitter { : new Set(); this.workerEntryPolls = mt.workerEntryPolls ?? null; this.stackChecks = mt.stackChecks ?? null; + this.loopCallees = findLoopCallees(mod); // A stackless call's eager prefix borrows its caller's stack; the // runtime moves deep prefix nests to a fresh stack using the guard, // which Windows lacks. @@ -2297,7 +2366,41 @@ export class LlEmitter { virtualEntry: (implFn) => this.virtualEntry(implFn), programLifetime: this.programLifetime, regionAliasTag: (word) => this.memoryAliasAttachment(`region.${word}`), + heapCensus: this.heapCensus, }; + // An instance of a class that extends Array is an array: a slot of its + // element array type can hold it, and the collector visits it there as + // an array, which has a collector header when its element ops trace + // (scr_arr_make_ops_kind). Such a class carries a header too, and so + // does the rest of its hierarchy. A class whose elements are instances + // of another such class follows that class, so repeat until no class + // adds one. + const arrayClasses = (mod.classes ?? []).filter((c) => c.arrayElem !== undefined); + if (arrayClasses.length > 0) { + const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base] as const)); + const rootOf = (name: string): string => { + let root = name; + for ( + let base = baseOf.get(root); + base !== undefined && baseOf.has(base); + base = baseOf.get(root) + ) + root = base; + return root; + }; + for (let changed = true; changed;) { + changed = false; + for (const cls of arrayClasses) { + if (this.tracedShapes.has(`object:${cls.name}`)) continue; + if (traceAdapter(this.shapeHost, { kind: "array", elem: cls.arrayElem! }) === null) + continue; + const root = rootOf(cls.name); + for (const member of mod.classes ?? []) + if (rootOf(member.name) === root) this.tracedShapes.add(`object:${member.name}`); + changed = true; + } + } + } this.walkers = new LlWalkers(this.shapeHost); this.dyn = new LlDyn(this.shapeHost); for (const g of mod.globals ?? []) { @@ -2385,6 +2488,41 @@ export class LlEmitter { mod.lib === undefined && options.lowRegion === true && [...this.classMeta.values()].some((m) => m.compact); + // Group arenas: off with lifetime regions, which own their classes' + // allocation, and in libraries, whose exports any host code calls. + if (arenasRequested() && mod.lifetimeRegions !== true && mod.lib === undefined) { + this.arenas = planArenas(mod, { + shape: (id) => { + const shape = this.recordsById.get(id); + return ( + shape !== undefined && + shape.freezable !== true && + shape.indexValue === undefined && + this.publishedTypes?.recordGuarded(id) !== true + ); + }, + cls: (def) => { + const meta = this.classMeta.get(def.name); + return ( + meta !== undefined && + !meta.compact && + !meta.threadSlot && + // An Array subclass's instance is an array (the runtime's header). + def.arrayElem === undefined && + meta.root.def.arrayElem === undefined && + !this.programLifetime.has(def.name) && + this.publishedTypes?.classGuarded(def.name) !== true + ); + }, + nullableArm: (unionId) => this.nullableUnions.single(unionId)?.arm ?? null, + traced: (t) => traceAdapter(this.shapeHost, t) !== null, + }); + if (this.arenas !== null) { + this.shapeHost.arenaShapes = this.arenas.shapes; + this.shapeHost.arenaClasses = this.arenas.classes; + this.shapeHost.arenasClosed = this.arenas.allClosed; + } + } for (const [name, rec] of RUNTIME_ERROR_CLASSES) { const meta = this.classMeta.get(name); if (!meta) break; // hand-written IR without the builtin defs: no stamps @@ -2513,7 +2651,8 @@ export class LlEmitter { emitParts(): string[] { const parts = emitParts(this); const bridged = this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts; - return this.cpuDispatch ? cloneForCpuDispatch(bridged) : bridged; + const out = this.cpuDispatch ? cloneForCpuDispatch(bridged) : bridged; + return this.heapCensus === null ? out : [...out, this.heapCensus.definitions().join("\n")]; } /** The dispose-observer gates the emitted frees and record writers test @@ -2686,19 +2825,21 @@ export class LlEmitter { } } - /** Chooses the native async functions whose bodies compile to switched - * coroutines instead of running on a fiber: a suspended call then keeps a - * heap frame of the values live across its awaits rather than a fiber and - * its stack. A body qualifies when its only suspension points are its own - * awaits, which the coroutine form suspends itself at (emitCoroAwait), so - * nothing it runs can park the stack it borrows. It is first emitted as - * usual: that text may suspend only through await runtime calls and - * await-call entries, which the coroutine form replaces, and may call no - * function that parks. The coroutine form is then emitted and must pass - * the same test with no exceptions (its frame bookkeeping excepted); - * otherwise the fiber form stays. Bodies that never suspend stay - * fiberless, and module evaluation bodies and `dyn` results keep fibers - * as they do there. The runtime traps if a stackless body ever parks. */ + /** Chooses the native async functions and async generators whose bodies + * compile to switched coroutines instead of running on a fiber: a + * suspended call then keeps a heap frame of the values live across its + * awaits and yields rather than a fiber and its stack. A body qualifies + * when its only suspension points are its own awaits and yields, which + * the coroutine form suspends itself at (emitCoroAwait, + * emitCoroSuspendPrepared), so nothing it runs can park the stack it + * borrows. It is first emitted as usual: that text may suspend only + * through await and yield runtime calls and await-call entries, which the + * coroutine form replaces, and may call no function that parks. The + * coroutine form is then emitted and must pass the same test with no + * exceptions (its frame bookkeeping excepted); otherwise the fiber form + * stays. Bodies that never suspend stay fiberless, and module evaluation + * bodies and async functions with `dyn` results keep fibers as they do + * there. The runtime traps if a stackless body ever parks. */ selectStacklessAsync(fnDefs: string[]): void { if (!this.stacklessAsyncAllowed) return; const parks = this.parkingFunctions(); @@ -2713,11 +2854,16 @@ export class LlEmitter { return true; }; this.mod.functions.forEach((fn, index) => { - if (fn.async !== true || fn.generator !== undefined) return; + if (fn.async !== true) return; if (fn.asyncCacheGlobal !== undefined || fn.asyncCycleCacheGlobal !== undefined) return; - if (fn.returnType.kind === "dyn" || !parks.has(fn.name)) return; - const fiberText = fnDefs[index]!; - if (!qualifies(fn, fiberText.replace(AWAIT_SUSPENSIONS, ""), SUSPENDS)) return; + // A dyn result may adopt a thenable when it settles the promise; an + // async generator's completion value settles its request's record. + if (fn.generator === undefined && fn.returnType.kind === "dyn") return; + if (!parks.has(fn.name)) return; + // An async generator's body also suspends at its own yields. + let fiberText = fnDefs[index]!.replace(AWAIT_SUSPENSIONS, ""); + if (fn.generator !== undefined) fiberText = fiberText.replace(YIELD_SUSPENSIONS, ""); + if (!qualifies(fn, fiberText, SUSPENDS)) return; this.stacklessAsync.add(fn.name); const coroText = this.emitFunction(fn); if (!qualifies(fn, coroText, COROUTINE_SUSPENDS)) { @@ -3130,7 +3276,7 @@ export class LlEmitter { * native async bodies selectStacklessAsync chose. */ coroLowered(fn: IrFunction): boolean { if (this.wasi) return fn.async === true || fn.generator !== undefined; - if (fn.async === true) return fn.generator === undefined && this.stacklessAsync.has(fn.name); + if (fn.async === true) return this.stacklessAsync.has(fn.name); return this.stacklessGenerators && fn.generator !== undefined; } emitGenScaffolding(): string[] { @@ -3500,6 +3646,7 @@ export class LlEmitter { /** Releases a statement frame's temps and owned slots. */ releaseTemps(frame: LlValue[]): void { for (const v of frame) { + if (v.arena !== undefined) continue; if (v.slot) { const t = this.B.tmp(); this.B.line(`${t} = load ptr, ptr ${v.name}`); @@ -3512,10 +3659,19 @@ export class LlEmitter { this.releaseFrame(v.name, v.type); } } + // Arena lists last: the frame's temps may be arena objects. + for (const v of frame) + if (v.arena === "drop") this.B.line(`call void @scr_arena_drop(ptr ${v.name})`); + else if (v.arena === "close") { + const head = this.B.tmp(); + this.B.line(`${head} = load ptr, ptr ${v.name}`); + this.B.line(`call void @scr_arena_close(ptr ${head})`); + } } releaseScope(scope: LlScopeEntry[]): void { for (const e of scope) { + if (e.arena === true) continue; const t = this.B.tmp(); this.B.line(`${t} = load ptr, ptr ${e.slot}`); if (e.boxed && e.lazy) { @@ -3537,6 +3693,9 @@ export class LlEmitter { this.releaseFrame(t, e.type); // runtime releases are NULL-tolerant } } + // Arena lists last: the scope's bindings may hold arena objects. + for (const e of scope) + if (e.arena === true) this.B.line(`call void @scr_arena_drop(ptr ${e.slot})`); } /** THE release-on-jump path (break/continue/return): pending statement @@ -3861,6 +4020,17 @@ export class LlEmitter { * calls (unwindAbiFunctions excludes their callees). */ emitDirectCall(line: string, callee: string): boolean { const B = this.B; + // A small leaf called only from loops inlines into each of them, past + // LLVM's cost threshold (loop-callees.ts), and so does a function called + // only from a loop's entry-range version (loop-entry-versions.ts). + // Debug builds keep the call. + if (this.debug === null && (this.loopCallees.has(callee) || loopEntryInlines(this, callee))) { + const comment = line.indexOf(" ; "); + line = + comment < 0 + ? `${line} alwaysinline` + : `${line.slice(0, comment)} alwaysinline${line.slice(comment)}`; + } if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) { B.line(line); return false; @@ -3991,10 +4161,22 @@ export class LlEmitter { ); } + /** The countdown slot of `loop`'s cancellation poll (emitWorkerLoopCheck), + * or null when the loop does not poll. An ELF worker executable's poll is + * one ordered load of the thread's own pending word. A loop nest keeps + * one countdown per loop, which its register pressure often leaves in + * memory, so the loops of a nest there poll on every iteration and get + * POLL_EVERY_ITERATION instead of a slot. A loop on its own keeps its + * countdown in a register, which costs less than the load. */ workerLoopBudget( loop: Extract, ): string | null { if (!this.workerLoopPolls(loop)) return null; + if ( + this.executableTls && + (this.jumpTargets.some((target) => target.kind === "loop") || containsLoop(loop.body)) + ) + return POLL_EVERY_ITERATION; const slot = this.B.slot(); this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`); this.B.line(`store i32 0, ptr ${slot}`); @@ -4099,6 +4281,10 @@ export class LlEmitter { * stay in the loop. */ emitWorkerLoopCheck(slot: string | null): void { if (slot === null) return; + if (slot === POLL_EVERY_ITERATION) { + this.emitWorkerPoll(); + return; + } const B = this.B; const budget = B.tmp(), due = B.tmp(), @@ -4118,9 +4304,9 @@ export class LlEmitter { } /** 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. */ + * their own checks. Outside ELF worker executables, 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); } @@ -4506,6 +4692,7 @@ export class LlEmitter { `${p} = getelementptr inbounds %${classStructSym(className)}, ptr ${objName}, i64 0, i32 ${index}`, ); this.markFieldPointer(p, field); + this.heapCensus?.countAccess((line) => this.B.line(line), className, field); if (compressed) this.compressedFieldPtrs.add(p); return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; } @@ -5185,7 +5372,7 @@ export class LlEmitter { /** Awaits in the current body suspend its coroutine: every wasm32 async * body, and native stackless async bodies (selectStacklessAsync). */ get coroAwait(): boolean { - return this.wasi || this.currentCoro?.kind === "async"; + return this.wasi || this.currentCoro?.awaits === true; } /** Suspends the current coroutine body at an await of `promise` (null: a @@ -5224,7 +5411,7 @@ export class LlEmitter { * coroutine has already suspended until the promise settled, so its * extraction takes no further turn. */ awaitResultFn(kind: "void" | "f64" | "bool" | "str" | "ref"): string { - return !this.wasi && this.currentCoro?.kind === "async" + return !this.wasi && this.currentCoro?.awaits === true ? `scr_coro_await_${kind}` : `scr_await_${kind}`; } @@ -6292,6 +6479,7 @@ export class LlEmitter { type: local.type, slot: true, ...(local.boxed ? { boxed: true } : {}), + ...(local.arena === true ? { arena: "drop" as const } : {}), }); } return result; @@ -7386,9 +7574,196 @@ export class LlEmitter { } emitExpr(e: IrExpr): LlValue { + return this.arenas === null ? emitExpr(this, e) : this.emitArenaExpr(e, this.arenas); + } + + /** emitExpr when group arenas are planned. Kept out of emitExpr, whose + * frame each level of a nested expression holds while it is emitted. */ + private emitArenaExpr(e: IrExpr, arenas: ArenaPlan): LlValue { + if (arenas.groups.calls.has(e)) { + // A call region: everything the call allocates dies when it returns. + // The statement drops the arena after its temps, which an + // arena-aware caller can have allocated in it while evaluating the + // arguments; any other caller drops it as soon as the call returns. + const slot = this.arenaSlot(e); + this.statementFrame().push({ name: slot, type: e.type, slot: true, arena: "drop" }); + this.B.line(`call ptr @scr_arena_open(ptr ${slot}, i32 ${this.arenaClosed(e)})`); + const value = emitExpr(this, e); + if (!arenaAware(arenas, this.fnByName.get(this.currentFunctionName))) + this.B.line(`call void @scr_arena_drop(ptr ${slot})`); + return value; + } + if (arenas.sites.heapCalls.has(e) && this.arenaSite(null)) { + // A call whose value the group does not keep: everything it allocates + // takes the heap. + const saved = this.suspendArena(); + const value = emitExpr(this, e); + this.resumeArena(saved); + return value; + } + if (this.opensArena(e)) { + // A root: its group lives until the scope holding its value exits + // (the statement frame for a value used in place). + const slot = this.arenaSlot(e); + const local = arenas.groups.drops.get(e) ?? null; + this.statementFrame().push({ + name: slot, + type: e.type, + slot: true, + arena: local === null ? "drop" : "close", + }); + const arena = this.B.tmp(); + this.B.line(`${arena} = call ptr @scr_arena_open(ptr ${slot}, i32 ${this.arenaClosed(e)})`); + const value = emitExpr(this, e); + this.B.line(`call void @scr_arena_close(ptr ${arena})`); + return value; + } return emitExpr(this, e); } + /** Group arenas (arenas.ts): the plan, the list slot of each root and + * call region, and which locals' scopes drop which roots' arenas. */ + arenas: ArenaPlan | null = null; + private arenaSlots = new Map(); + /** The frames emitStmtBody opens for whole statements (arenas only). */ + readonly statementFrames = new Set(); + + /** The frame of the statement being emitted: a nested frame (call + * arguments, branches) ends before the values built in it do. */ + statementFrame(): LlValue[] { + for (let i = this.frames.length - 1; i >= 0; i--) + if (this.statementFrames.has(this.frames[i]!)) return this.frames[i]!; + return this.currentFrame(); + } + + /** The arena list slot of root or call region `e`: allocated, cleared and + * (for a root dropped with a local) owned by that local's scope when the + * function starts or the local is declared. */ + arenaSlot(e: Node): string { + let slot = this.arenaSlots.get(e); + if (slot === undefined) { + slot = this.B.tmp(); + this.B.entryAllocas.push(`${slot} = alloca ptr`, `store ptr null, ptr ${slot}`); + this.arenaSlots.set(e, slot); + this.declare(`declare ptr @scr_arena_open(ptr, i32)`); + this.declare(`declare void @scr_arena_close(ptr)`); + this.declare(`declare void @scr_arena_drop(ptr)`); + } + return slot; + } + + /** Whether emitExpr opens an arena for `e`: a root call or `new` of a + * counted value. */ + opensArena(e: Node): e is Extract { + return ( + this.arenas !== null && + this.arenas.groups.roots.has(e) && + (e.kind === "call" || e.kind === "new") && + isRefCounted(e.type) + ); + } + + /** 1 when the group of root or call region `e` is closed: its objects + * hold only each other and immortal values. */ + arenaClosed(e: Node): number { + return this.arenas?.groups.closed.has(e) === true ? 1 : 0; + } + + /** Registers on the innermost scope the arena lists of the roots whose + * arenas `localId`'s scope drops. */ + ownArenasOf(localId: string): void { + const roots = this.arenaDropsByLocal.get(localId); + if (roots === undefined) return; + const scope = this.scopes[this.scopes.length - 1]!; + for (const { root, type } of roots) + scope.push({ slot: this.arenaSlot(root), type, arena: true }); + } + + /** The roots of the function being emitted, by the local that drops + * them; set when a function's emission starts. */ + arenaDropsByLocal = new Map(); + + /** Starts a function's arena bookkeeping: its roots by dropping local, + * with the lists of locals the body never declares (parameters, loop + * and catch bindings) owned by the function's own scope. */ + beginArenaFunction(fn: IrFunction): void { + this.arenaSlots.clear(); + this.statementFrames.clear(); + this.arenaDropsByLocal = new Map(); + const arenas = this.arenas; + if (arenas === null) return; + const declared = new Set(); + const visit = (node: Node): void => { + if (node.kind === "varDecl") declared.add(node.localId); + if (this.opensArena(node)) { + const local = arenas.groups.drops.get(node); + if (local !== undefined && local !== null) { + const list = this.arenaDropsByLocal.get(local) ?? []; + list.push({ root: node, type: node.type }); + this.arenaDropsByLocal.set(local, list); + } + } + const kids: Node[] = []; + children(node, kids); + for (const kid of kids) visit(kid); + }; + for (const s of fn.body) visit(s); + for (const local of [...this.arenaDropsByLocal.keys()]) + if (!declared.has(local)) this.ownArenasOf(local); + } + + /** Whether allocation site `e` (null: any) in the function being + * emitted takes the open arena: the function is arena-aware and the group + * keeps the site's value (region-sites.ts). */ + arenaSite(e: IrExpr | null): boolean { + const arenas = this.arenas; + if (!arenaAware(arenas, this.fnByName.get(this.currentFunctionName))) return false; + return e === null || arenas!.sites.arenaSites.has(e); + } + + /** Stops any arena being the allocation target; returns what restores it. */ + suspendArena(): string { + const saved = this.B.tmp(); + const cur = this.B.tmp(); + const lines = arenaCurrentAddress(this.shapeHost, cur); + if (lines === null) { + this.declare(`declare ptr @scr_arena_suspend()`); + this.B.line(`${saved} = call ptr @scr_arena_suspend()`); + } else { + for (const line of lines) this.B.line(line); + this.B.line(`${saved} = load ptr, ptr ${cur}`); + this.B.line(`store ptr null, ptr ${cur}`); + } + return saved; + } + + /** Makes the arena suspendArena stopped (`saved`) the target again. */ + resumeArena(saved: string): void { + const cur = this.B.tmp(); + const lines = arenaCurrentAddress(this.shapeHost, cur); + if (lines === null) { + this.declare(`declare void @scr_arena_resume(ptr)`); + this.B.line(`call void @scr_arena_resume(ptr ${saved})`); + } else { + for (const line of lines) this.B.line(line); + this.B.line(`store ptr ${saved}, ptr ${cur}`); + } + } + + /** The arena-aware constructor of `newSym` for an allocation of a + * record shape or class in the function being emitted, or `newSym`. */ + allocSym(newSym: string, e: IrExpr): string { + const arenas = this.arenas; + if (arenas === null) return newSym; + const planned = + e.type.kind === "record" + ? arenas.shapes.has(e.type.shapeId) + : e.kind === "new" && arenas.classes.has(e.className); + if (!planned) return newSym; + const aware = this.arenaSite(e) || arenas.groups.roots.has(e); + return aware ? arenaNewSym(newSym) : newSym; + } + /** The helper classification of this module's inline-cache sites. */ get jsIcSites(): JsIcSites { return (this.jsIcSitesMemo ??= new JsIcSites(this.mod)); @@ -7479,15 +7854,15 @@ export class LlEmitter { return wrapEmitterListener(this, target, adapterFn); } - /** 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 + /** ScrClosure.function_kind for a closure over `fn`: its generator, + * async and own prototype bits, and RECEIVER_FREE 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) + (fn.generator ? CLOSURE_KIND.GENERATOR : 0) + + (fn.async ? CLOSURE_KIND.ASYNC : 0) + + (fn.ownsPrototype ? CLOSURE_KIND.OWN_PROTOTYPE : 0) + + (this.receiverReaders.ignores(fn) ? CLOSURE_KIND.RECEIVER_FREE : 0) ); } @@ -7678,6 +8053,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( diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 037179d8e..335946d9d 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -409,12 +409,24 @@ 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, note); + // 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(undefined, false, undefined, note); return out; diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index b380767d4..f0ebe7c77 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -1,9 +1,10 @@ /* 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 type { LlvmEmitterContext, LlValue, OffsetIndex } from "./expr-context.js"; +import { F64_INF, exactIndexLines, exactOffsetIndexLines, f64Lit, sizeToDouble } from "./common.js"; import { alwaysExactInteger, exactInteger } from "./integer-values.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { emitByteNumber } from "./byte-numbers.js"; @@ -76,6 +77,27 @@ export function emitIntegerLoopIndex(host: LlvmEmitterContext, expr: IrExpr): st return index; } +/** The base and offset of a sum `base + c`, `c + base` or `base - c`, + * where `base` is a number local and `c` an integer literal of magnitude + * below 2^31, given the operand values the sum was computed from. Null for + * other sums. */ +export function offsetIndex(expr: IrExpr, left: LlValue, right: LlValue): OffsetIndex | null { + if (expr.kind !== "bin" || expr.type.kind !== "f64" || (expr.op !== "+" && expr.op !== "-")) + return null; + const literal = (e: IrExpr): number | null => + e.kind === "numLit" && Number.isInteger(e.value) && Math.abs(e.value) < 2 ** 31 + ? e.value + : null; + const local = (e: IrExpr): boolean => e.kind === "varRef" && e.type.kind === "f64"; + const constant = literal(expr.right); + if (constant !== null && local(expr.left)) + return { base: left.name, offset: expr.op === "+" ? constant : -constant }; + const leading = literal(expr.left); + if (expr.op === "+" && leading !== null && local(expr.right)) + return { base: right.name, offset: leading }; + return null; +} + export function emitBytesIndex( host: LlvmEmitterContext, receiver: string, @@ -133,10 +155,33 @@ export function emitBytesIndex( B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`); wide = nanIndex.wide; } else { - const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); - for (const line of exact.lines) B.line(line); - ok = exact.ok; - wide = exact.wide; + const offset = value.offsetIndex; + if (offset) { + const exact = exactOffsetIndexLines(offset.base, offset.offset, len, host.sizeType, () => + B.tmp(), + ); + for (const line of exact.lines) B.line(line); + // A fractional base: the sum itself decides, as it would without the + // base's conversion. + const from = B.currentLabel(); + const sum = B.newLabel("bytes.index.sum"); + const join = B.newLabel("bytes.index.join"); + B.condBr(exact.integral, join, sum); + B.startBlock(sum); + const whole = exactIndexLines(index, len, host.sizeType, () => B.tmp()); + for (const line of whole.lines) B.line(line); + B.br(join); + B.startBlock(join); + ok = B.tmp(); + wide = B.tmp(); + B.line(`${ok} = phi i1 [ ${exact.ok}, %${from} ], [ ${whole.ok}, %${sum} ]`); + B.line(`${wide} = phi i64 [ ${exact.wide}, %${from} ], [ ${whole.wide}, %${sum} ]`); + } else { + const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp()); + for (const line of exact.lines) B.line(line); + ok = exact.ok; + wide = exact.wide; + } } const invalid = B.newLabel("bytes.index.invalid"); const valid = B.newLabel("bytes.index.valid"); @@ -160,6 +205,22 @@ export function emitBytesIndex( return idx; } +/** 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. */ @@ -168,6 +229,8 @@ export function emitBytesHeaderLength( 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); @@ -177,6 +240,8 @@ export function emitBytesHeaderLength( } 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`); @@ -203,6 +268,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(); @@ -303,7 +370,9 @@ function emitScopedBytesGet( const signed = elem === "i8" || elem === "i16" || elem === "i32"; B.line(`${number} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`); B.line(`${out} = select i1 ${valid}, double ${number}, double ${f64Lit(NaN)}`); - return { name: out, type: F64, uint32: u32, ordered: valid }; + 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( @@ -317,7 +386,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); } @@ -328,6 +397,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 @@ -666,7 +760,7 @@ function emitScopedBytesSet( ty = "i32"; operand = stored!; } - if (host.mod.workers && !owned) { + if (host.mod.workers && !owned && !plainSharedElement(host, elem, indexExpr)) { // A shared view's element takes a relaxed atomic store of its width. const local = emitSharedBytesSplit(host, receiver); const p = B.tmp(); @@ -788,6 +882,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)); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 3ecf33a79..9b2f2e4d8 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 { @@ -43,7 +44,7 @@ import { import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { - CLOSURE_RECEIVER_FREE, + CLOSURE_KIND, f64Lit, ffiNativeTypeLl, ffiNativeParamLl, @@ -818,7 +819,7 @@ export function emitCallExpr( `${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(`${bit} = and i32 ${from}, ${CLOSURE_KIND.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}`); @@ -937,7 +938,7 @@ export function emitCallExpr( ) : args; // An entry through the closure's own pointer brackets the call only - // when the closure may observe the receiver (SCR_FN_RECEIVER_FREE). + // when the closure may observe the receiver (SCR_CLOSURE_KIND_RECEIVER_FREE). const invoke = ( target: string, bracket: boolean, @@ -957,7 +958,7 @@ export function emitCallExpr( `${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(`${free} = and i32 ${kind}, ${CLOSURE_KIND.RECEIVER_FREE}`); B.line(`${observes} = icmp eq i32 ${free}, 0`); const push = B.newLabel("receiver.push"), pushed = B.newLabel("receiver.call"); @@ -1103,7 +1104,8 @@ export function emitCallExpr( `llvm emitter bug: new ${e.className} without a constructor`, ); const o = B.tmp(); - B.line(`${o} = call ptr @${mangleClassNew(e.className)}()`); + B.line(`${o} = call ptr @${host.allocSym(mangleClassNew(e.className), e)}()`); + host.heapCensus?.countNew((line) => B.line(line), e.className); const out = host.own({ name: o, type: e.type }); emitConstruction(host, e, o, ctor); return out; @@ -1235,6 +1237,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") diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 7eec14e4e..279afdcd0 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -62,12 +62,14 @@ import { import { exactInteger, widenInteger } from "./integer-values.js"; import { emitDenseArrayNumber, + emitDenseArrayNumberInt32, emitDenseArrayPop, emitDenseArrayPush, runtimeAccess, type ArrayAccess, } from "./dense-array-access.js"; import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js"; +import { INT32_RANGE } from "../../ir/integer-ranges.js"; import { emitUnionWiden } from "./expr-records.js"; import { registerReplay } from "./emit-journal.js"; @@ -694,6 +696,25 @@ export function emitArrIntrinsic( )!; return { name: value, type: e.type }; } + // In a store statement of an int32 kind loop's fast copy, a read that + // is not an int32 (a missing element's NaN) leaves for the + // statement's resume point, so the element is at hand as an i32. + if (!table && elem.kind === "f64" && access === "i32" && host.int32KindResume !== null) { + const read = emitDenseArrayNumberInt32( + host, + r.name, + index, + e.args[0], + host.provenDenseIndex(e.receiver, e.args[0]!), + host.int32KindResume, + ); + return { + name: read.value, + type: e.type, + uint32: read.int32, + integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE }, + }; + } if (!table && elem.kind === "f64") return { name: emitDenseArrayNumber( diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index aea9e4f50..394db8a2f 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -25,9 +25,26 @@ export interface LlValue { * 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; + /** The sum `base + offset` of a number local and an integer literal of + * magnitude below 2^31: the base as the sum read it. Typed array indexes + * convert it instead of the sum (expr-bytes.ts). */ + offsetIndex?: OffsetIndex; slot?: boolean; /** A sequence local held in a capture box; release the box itself. */ boxed?: boolean; + /** A group arena list slot (arenas.ts): the frame closes the newest + * arena on it, or drops the whole list, after its other temps. */ + arena?: "close" | "drop"; +} + +export interface OffsetIndex { + base: string; + offset: number; } export interface WideInteger { diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index 6e0fd4acb..28281478a 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -11,6 +11,10 @@ import { emitRecordKeyGetOr } from "./expr-records.js"; import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js"; import { emitUnionTag } from "./union-repr.js"; +/** Each kind emits in a function of its own: emitting a nested chain of + * these expressions holds one emitControlExpr frame per level, and in an + * unoptimized build of the compiler a frame holds every local of the + * function. */ export function emitControlExpr( host: LlvmEmitterContext, e: ExprOf< @@ -25,446 +29,25 @@ export function emitControlExpr( | "nullish" >, ): LlValue { - const B = host.B; switch (e.kind) { - case "dynDestrCheck": { - // RequireObjectCoercible with V8's destructuring TypeError. dyn - // values check in the runtime helper and pass through unchanged. - const v = host.emitExpr(e.value); - const first = e.firstProp !== undefined ? host.cstr(e.firstProp) : "null"; - - const helper = host.dyn.dynDestrCheckHelper(); - B.line(`call void @${helper}(ptr ${v.name}, ptr ${host.cstr(e.spelling)}, ptr ${first})`); - host.emitPendingCheck(); - return v; - } - case "dynIterN": { - // GetIterator + first-N steps as a fresh array (V8's exact - // not-iterable TypeError on non-iterables) through the native dyn helper. - const v = emitBorrowedInput(host, e.value); - - const helper = host.dyn.dynIterNHelper(); - const t = B.tmp(); - B.line( - `${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count}, ptr ${e.notIterableMessage ? host.cstr(e.notIterableMessage) : "null"})`, - ); - const out = host.own({ name: t, type: e.type }); - host.emitPendingCheck(); - return out; - } + case "dynDestrCheck": + return emitDynDestrCheck(host, e); + case "dynIterN": + return emitDynIterN(host, e); 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 - // and the right runs in its own frame. - const ty = host.llType(e.type); - const l = host.emitExpr(e.left); - host.toFrame(l); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - B.line(`store ${ty} ${l.name}, ptr ${slot}`); - const integerSlot = e.type.kind === "f64" && l.uint32 !== undefined ? B.slot() : undefined; - if (integerSlot !== undefined) { - B.entryAllocas.push(`${integerSlot} = alloca i32`); - B.line(`store i32 ${l.uint32!}, ptr ${integerSlot}`); - } - const truthy = host.truthy(l); - const rightLabel = B.newLabel("log.r"); - const joinLabel = B.newLabel("log.j"); - if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel); - else B.condBr(truthy, joinLabel, rightLabel); - B.startBlock(rightLabel); - if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type); - const rightInteger = host.emitBranchInto(slot, e.right, integerSlot); - B.br(joinLabel); - B.startBlock(joinLabel); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - if (integerSlot !== undefined && rightInteger) { - const uint32 = B.tmp(); - B.line(`${uint32} = load i32, ptr ${integerSlot}`); - return { name: t, type: e.type, uint32 }; - } - return host.own({ name: t, type: e.type }); - } - case "ternary": { - // Exactly one arm evaluates; each arm runs in its own frame and - // moves the chosen value into the result slot. - const ty = host.llType(e.type); - const c = host.emitExpr(e.cond); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const integerSlot = e.type.kind === "f64" ? B.slot() : undefined; - if (integerSlot !== undefined) B.entryAllocas.push(`${integerSlot} = alloca i32`); - const lt = B.newLabel("tern.t"); - const lf = B.newLabel("tern.f"); - const lj = B.newLabel("tern.j"); - B.condBr(c.name, lt, lf); - B.startBlock(lt); - const thenInteger = host.emitBranchInto(slot, e.then, integerSlot); - B.br(lj); - B.startBlock(lf); - const elseInteger = host.emitBranchInto(slot, e.else_, integerSlot); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - // A view is available only when every incoming branch wrote it. - // Incomplete views are unused and disappear during promotion. - if (integerSlot !== undefined && thenInteger && elseInteger) { - const uint32 = B.tmp(); - B.line(`${uint32} = load i32, ptr ${integerSlot}`); - return { name: t, type: e.type, uint32 }; - } - // A choice between interned literals is an interned literal: like a - // strLit operand, nothing owns it (retain and release are no-ops). - if (literalChoice(e)) return host.markImmortal({ name: t, type: e.type }); - return host.own({ name: t, type: e.type }); - } - case "optChain": { - // `a?.b` / `f?.()`: the nullish test inverted. The receiver - // evaluates once into an ordinary frame temp (borrowed); on a unit - // tag the result is the interned undefined arm and the body never - // runs; otherwise the narrowed payload fills the bind slot (+1, - // frame-owned through a SLOT entry — NULL on the unit path, where - // the frame's release is a no-op) and the body reads it through - // chainRecv. - if (e.receiver.type.kind === "dyn") { - // A dyn (dyn) receiver — the `rawName?.match(re)` step: the - // nullish test reads the node's kind tag; the unit path is the - // undefined dyn singleton (dyn results) or nothing (void - // bodies), the body runs over the bound receiver otherwise. - const r = host.emitExpr(e.receiver); - const kd = host.dynKind(r.name); - const isU = B.tmp(); - const isN = B.tmp(); - const isUnit = B.tmp(); - B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`); - B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`); - B.line(`${isUnit} = or i1 ${isU}, ${isN}`); - const bind = B.slot(); - B.entryAllocas.push(`${bind} = alloca ptr`); - B.line(`store ptr null, ptr ${bind}`); - host.ownSlot(bind, e.receiver.type); - host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); - if (e.type.kind === "void") { - const lb = B.newLabel("ocd.b"); - const lj = B.newLabel("ocd.j"); - B.condBr(isUnit, lj, lb); - B.startBlock(lb); - const rr = B.tmp(); - B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`); - B.line(`store ptr ${rr}, ptr ${bind}`); - host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); - host.frames.push([]); - host.emitExpr(e.body); - host.releaseTemps(host.frames.pop()!); - host.chainSlots.delete(e.id); - B.br(lj); - B.startBlock(lj); - return { name: "", type: e.type }; - } - if (e.type.kind !== "dyn") - throw new InternalCompilerError("llvm emitter bug: dyn optChain result kind"); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ptr`); - const lu = B.newLabel("ocd.u"); - const lb = B.newLabel("ocd.b"); - const lj = B.newLabel("ocd.j"); - B.condBr(isUnit, lu, lb); - B.startBlock(lu); - host.declare(`declare ptr @scr_dyn_undefined()`); - const un = B.tmp(); - const ur = B.tmp(); - B.line(`${un} = call ptr @scr_dyn_undefined()`); - B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`); - B.line(`store ptr ${ur}, ptr ${slot}`); - B.br(lj); - B.startBlock(lb); - const rr = B.tmp(); - B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`); - B.line(`store ptr ${rr}, ptr ${bind}`); - host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); - host.emitBranchInto(slot, e.body); - host.chainSlots.delete(e.id); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - - const { isUnit, bindPresent } = emitOptionalChainReceiver(host, e); - if (e.type.kind === "void") { - // Statement form (cb?.()): no result value at all. - const lb = B.newLabel("oc.b"); - const lj = B.newLabel("oc.j"); - B.condBr(isUnit, lj, lb); - B.startBlock(lb); - bindPresent(); - host.frames.push([]); - host.emitExpr(e.body); - host.releaseTemps(host.frames.pop()!); - host.chainSlots.delete(e.id); - B.br(lj); - B.startBlock(lj); - return { name: "", type: e.type }; - } - if (e.type.kind === "dyn") { - // A dyn-typed chain (`pricing?.[key]` over an unknown-valued - // index signature): the unit path is the undefined dyn value — - // dyn represents undefined directly, no union wrapper exists. - const slotD = B.slot(); - B.entryAllocas.push(`${slotD} = alloca ptr`); - const lu = B.newLabel("ocu.u"); - const lb = B.newLabel("ocu.b"); - const lj = B.newLabel("ocu.j"); - B.condBr(isUnit, lu, lb); - B.startBlock(lu); - host.declare(`declare ptr @scr_dyn_undefined()`); - host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); - const un = B.tmp(); - const ur = B.tmp(); - B.line(`${un} = call ptr @scr_dyn_undefined()`); - B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`); - B.line(`store ptr ${ur}, ptr ${slotD}`); - B.br(lj); - B.startBlock(lb); - bindPresent(); - host.emitBranchInto(slotD, e.body); - host.chainSlots.delete(e.id); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${slotD}`); - return host.own({ name: t, type: e.type }); - } - if (e.type.kind !== "union") - throw new LlvmUnsupportedError(`optChainResult:${e.type.kind}`, e.loc); - const undefTag = undefinedArmTag(e.type, host.unionsById); - if (undefTag < 0) - throw new InternalCompilerError( - "llvm emitter bug: optChain result lacks its undefined arm", - ); - const ty = host.llType(e.type); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const lu = B.newLabel("oc.u"); - const lb = B.newLabel("oc.b"); - const lj = B.newLabel("oc.j"); - B.condBr(isUnit, lu, lb); - B.startBlock(lu); - B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`); - B.br(lj); - B.startBlock(lb); - bindPresent(); - host.emitBranchInto(slot, e.body); - host.chainSlots.delete(e.id); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - case "chainRecv": { - const bound = host.chainSlots.get(e.id); - if (!bound) - throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`); - const v = B.tmp(); - B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`); - if (!isRefCounted(e.type)) return { name: v, type: e.type }; - return host.own({ name: host.retainFrame(v, e.type), type: e.type }); - } - case "orDefault": { - // `u || d` narrowed to the single non-unit arm: nullish's dance - // with the union TRUTHY switch as the test — truthy extracts the - // arm (+1 for ref kinds), falsy releases and runs the default. - if (e.left.type.kind !== "union") - throw new InternalCompilerError("llvm emitter bug: orDefault left is not a union"); - if ( - e.retag === undefined && - !host.nullableUnions.has(e.left.type.unionId) && - host.isStackUnionSource(e.left) - ) { - // A stack box (`arr[i] || d`): test truthiness and extract the - // payload (+1). The box is never retained or released. - const l = host.emitUnionProjection(e.left); - const ty = host.llType(e.type); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const lt = B.newLabel("ord.t"), - lf = B.newLabel("ord.f"), - lj = B.newLabel("ord.j"); - B.condBr(host.truthy(l), lt, lf); - B.startBlock(lt); - B.line( - `store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`, - ); - B.br(lj); - B.startBlock(lf); - host.emitBranchInto(slot, e.right); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - const l = host.emitExpr(e.left); - host.toFrame(l); - const ty = host.llType(e.type); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const truthy = host.truthy(l); - const lt = B.newLabel("ord.t"); - const lf = B.newLabel("ord.f"); - const lj = B.newLabel("ord.j"); - B.condBr(truthy, lt, lf); - B.startBlock(lt); - if (e.retag !== undefined) { - // Retagged shape: the whole box goes to the union→union helper, - // which CONSUMES it (callees own their params) — no release on - // this side. The store precedes the pending check so an unwind - // leaves only the NULL dummy behind, in a slot nothing reads. - const t = B.tmp(); - B.line( - `${t} = call ${ty} @${host.callTarget(e.retag)}(${host.llType(e.left.type)} ${l.name})`, - ); - B.line(`store ${ty} ${t}, ptr ${slot}`); - if (host.mayThrow.has(e.retag)) host.emitPendingCheck(); - } else if (host.nullableUnions.single(e.left.type.unionId)) { - // The owned nullable pointer IS the owned arm value. - B.line(`store ${ty} ${l.name}, ptr ${slot}`); - } else { - B.line( - `store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`, - ); - host.releaseFrame(l.name, e.left.type); - } - B.br(lj); - B.startBlock(lf); - host.releaseFrame(l.name, e.left.type); - host.emitBranchInto(slot, e.right); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - case "nullish": { - // `a ?? b`: logical's move/release dance with the left's runtime - // TAG against its unit arms as the test. Pass-through shape: the - // result IS the left box. Narrowed shape: the single non-unit - // arm's payload extracts (+1 for ref kinds) and the box releases. - - if (e.left.type.kind === "dyn") { - // The native checked-value kind decides whether the left is - // nullish; the right runs lazily in its branch. - const l = host.emitExpr(e.left); - host.toFrame(l); - host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ptr`); - const isN = B.tmp(); - B.line(`${isN} = call zeroext i1 @scr_dyn_is_nullish(ptr ${l.name})`); - const lu = B.newLabel("nuld.u"); - const lv = B.newLabel("nuld.v"); - const lj = B.newLabel("nuld.j"); - B.condBr(isN, lu, lv); - B.startBlock(lu); - host.releaseFrame(l.name, e.left.type); - host.emitBranchInto(slot, e.right); - B.br(lj); - B.startBlock(lv); - B.line(`store ptr ${l.name}, ptr ${slot}`); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ptr, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - if (e.left.type.kind !== "union") - throw new InternalCompilerError("llvm emitter bug: nullish left is not a union"); - const def = host.unionsById.get(e.left.type.unionId); - if (!def) - throw new InternalCompilerError( - `llvm emitter bug: nullish of unknown union ${e.left.type.unionId}`, - ); - const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : [])); - if (unitTags.length === 0) - 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" || - host.isStackUnionSource(e.left) || - host.nullableFieldGet(e.left) !== null) - ) { - // Narrowed shape over a binding or a stack box (`arr[i] ?? d`, - // `map.get(k) ?? d`): test the tag and extract the payload (+1). - // The box is never retained or released. A binding is borrowed: - // no code runs between its read and the extraction, and the - // default never touches it. Bindings proven projection-only may - // hold stack boxes, which must never reach RC entry points. - const l = host.emitUnionProjection(e.left); - const ty = host.llType(e.type); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const isUnit = host.tagInSet(l.name, def.id, unitTags); - const lu = B.newLabel("nul.u"); - const lv = B.newLabel("nul.v"); - const lj = B.newLabel("nul.j"); - B.condBr(isUnit, lu, lv); - B.startBlock(lu); - host.emitBranchInto(slot, e.right); - B.br(lj); - B.startBlock(lv); - B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } - const l = host.emitExpr(e.left); - host.toFrame(l); - const ty = host.llType(e.type); - const slot = B.slot(); - B.entryAllocas.push(`${slot} = alloca ${ty}`); - const isUnit = host.tagInSet(l.name, def.id, unitTags); - const lu = B.newLabel("nul.u"); - const lv = B.newLabel("nul.v"); - const lj = B.newLabel("nul.j"); - B.condBr(isUnit, lu, lv); - B.startBlock(lu); - host.releaseFrame(l.name, e.left.type); - host.emitBranchInto(slot, e.right); - B.br(lj); - B.startBlock(lv); - if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) { - // Pass-through, or a nullable pointer that IS its owned arm value. - B.line(`store ${ty} ${l.name}, ptr ${slot}`); - } else { - B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); - host.releaseFrame(l.name, e.left.type); - } - B.br(lj); - B.startBlock(lj); - const t = B.tmp(); - B.line(`${t} = load ${ty}, ptr ${slot}`); - return host.own({ name: t, type: e.type }); - } + case "logical": + return emitLogical(host, e); + case "ternary": + return emitTernary(host, e); + case "optChain": + return emitOptChain(host, e); + case "chainRecv": + return emitChainRecv(host, e); + case "orDefault": + return emitOrDefault(host, e); + case "nullish": + return emitNullish(host, e); default: { const _exhaustive: never = e; void _exhaustive; @@ -473,6 +56,450 @@ export function emitControlExpr( } } +function emitDynDestrCheck(host: LlvmEmitterContext, e: ExprOf<"dynDestrCheck">): LlValue { + const B = host.B; + // RequireObjectCoercible with V8's destructuring TypeError. dyn + // values check in the runtime helper and pass through unchanged. + const v = host.emitExpr(e.value); + const first = e.firstProp !== undefined ? host.cstr(e.firstProp) : "null"; + + const helper = host.dyn.dynDestrCheckHelper(); + B.line(`call void @${helper}(ptr ${v.name}, ptr ${host.cstr(e.spelling)}, ptr ${first})`); + host.emitPendingCheck(); + return v; +} + +function emitDynIterN(host: LlvmEmitterContext, e: ExprOf<"dynIterN">): LlValue { + const B = host.B; + // GetIterator + first-N steps as a fresh array (V8's exact + // not-iterable TypeError on non-iterables) through the native dyn helper. + const v = emitBorrowedInput(host, e.value); + + const helper = host.dyn.dynIterNHelper(); + const t = B.tmp(); + B.line( + `${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count}, ptr ${e.notIterableMessage ? host.cstr(e.notIterableMessage) : "null"})`, + ); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; +} + +function emitLogical(host: LlvmEmitterContext, e: ExprOf<"logical">): LlValue { + const B = host.B; + 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 + // and the right runs in its own frame. + const ty = host.llType(e.type); + const l = host.emitExpr(e.left); + host.toFrame(l); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + B.line(`store ${ty} ${l.name}, ptr ${slot}`); + const integerSlot = e.type.kind === "f64" && l.uint32 !== undefined ? B.slot() : undefined; + if (integerSlot !== undefined) { + B.entryAllocas.push(`${integerSlot} = alloca i32`); + B.line(`store i32 ${l.uint32!}, ptr ${integerSlot}`); + } + const truthy = host.truthy(l); + const rightLabel = B.newLabel("log.r"); + const joinLabel = B.newLabel("log.j"); + if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel); + else B.condBr(truthy, joinLabel, rightLabel); + B.startBlock(rightLabel); + if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type); + const rightInteger = host.emitBranchInto(slot, e.right, integerSlot); + B.br(joinLabel); + B.startBlock(joinLabel); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + if (integerSlot !== undefined && rightInteger) { + const uint32 = B.tmp(); + B.line(`${uint32} = load i32, ptr ${integerSlot}`); + return { name: t, type: e.type, uint32 }; + } + return host.own({ name: t, type: e.type }); +} + +function emitTernary(host: LlvmEmitterContext, e: ExprOf<"ternary">): LlValue { + const B = host.B; + // Exactly one arm evaluates; each arm runs in its own frame and + // moves the chosen value into the result slot. + const ty = host.llType(e.type); + const c = host.emitExpr(e.cond); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const integerSlot = e.type.kind === "f64" ? B.slot() : undefined; + if (integerSlot !== undefined) B.entryAllocas.push(`${integerSlot} = alloca i32`); + const lt = B.newLabel("tern.t"); + const lf = B.newLabel("tern.f"); + const lj = B.newLabel("tern.j"); + B.condBr(c.name, lt, lf); + B.startBlock(lt); + const thenInteger = host.emitBranchInto(slot, e.then, integerSlot); + B.br(lj); + B.startBlock(lf); + const elseInteger = host.emitBranchInto(slot, e.else_, integerSlot); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + // A view is available only when every incoming branch wrote it. + // Incomplete views are unused and disappear during promotion. + if (integerSlot !== undefined && thenInteger && elseInteger) { + const uint32 = B.tmp(); + B.line(`${uint32} = load i32, ptr ${integerSlot}`); + return { name: t, type: e.type, uint32 }; + } + // A choice between interned literals is an interned literal: like a + // strLit operand, nothing owns it (retain and release are no-ops). + if (literalChoice(e)) return host.markImmortal({ name: t, type: e.type }); + return host.own({ name: t, type: e.type }); +} + +function emitOptChain(host: LlvmEmitterContext, e: ExprOf<"optChain">): LlValue { + const B = host.B; + // `a?.b` / `f?.()`: the nullish test inverted. The receiver + // evaluates once into an ordinary frame temp (borrowed); on a unit + // tag the result is the interned undefined arm and the body never + // runs; otherwise the narrowed payload fills the bind slot (+1, + // frame-owned through a SLOT entry — NULL on the unit path, where + // the frame's release is a no-op) and the body reads it through + // chainRecv. + if (e.receiver.type.kind === "dyn") { + // A dyn (dyn) receiver — the `rawName?.match(re)` step: the + // nullish test reads the node's kind tag; the unit path is the + // undefined dyn singleton (dyn results) or nothing (void + // bodies), the body runs over the bound receiver otherwise. + const r = host.emitExpr(e.receiver); + const kd = host.dynKind(r.name); + const isU = B.tmp(); + const isN = B.tmp(); + const isUnit = B.tmp(); + B.line(`${isU} = icmp eq i32 ${kd}, ${DYN_KIND.UNDEF}`); + B.line(`${isN} = icmp eq i32 ${kd}, ${DYN_KIND.NULL}`); + B.line(`${isUnit} = or i1 ${isU}, ${isN}`); + const bind = B.slot(); + B.entryAllocas.push(`${bind} = alloca ptr`); + B.line(`store ptr null, ptr ${bind}`); + host.ownSlot(bind, e.receiver.type); + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + if (e.type.kind === "void") { + const lb = B.newLabel("ocd.b"); + const lj = B.newLabel("ocd.j"); + B.condBr(isUnit, lj, lb); + B.startBlock(lb); + const rr = B.tmp(); + B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`); + B.line(`store ptr ${rr}, ptr ${bind}`); + host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); + host.frames.push([]); + host.emitExpr(e.body); + host.releaseTemps(host.frames.pop()!); + host.chainSlots.delete(e.id); + B.br(lj); + B.startBlock(lj); + return { name: "", type: e.type }; + } + if (e.type.kind !== "dyn") + throw new InternalCompilerError("llvm emitter bug: dyn optChain result kind"); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ptr`); + const lu = B.newLabel("ocd.u"); + const lb = B.newLabel("ocd.b"); + const lj = B.newLabel("ocd.j"); + B.condBr(isUnit, lu, lb); + B.startBlock(lu); + host.declare(`declare ptr @scr_dyn_undefined()`); + const un = B.tmp(); + const ur = B.tmp(); + B.line(`${un} = call ptr @scr_dyn_undefined()`); + B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`); + B.line(`store ptr ${ur}, ptr ${slot}`); + B.br(lj); + B.startBlock(lb); + const rr = B.tmp(); + B.line(`${rr} = call ptr @scr_dyn_retain_v(ptr ${r.name})`); + B.line(`store ptr ${rr}, ptr ${bind}`); + host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); + host.emitBranchInto(slot, e.body); + host.chainSlots.delete(e.id); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${slot}`); + return host.own({ name: t, type: e.type }); + } + + const { isUnit, bindPresent } = emitOptionalChainReceiver(host, e); + if (e.type.kind === "void") { + // Statement form (cb?.()): no result value at all. + const lb = B.newLabel("oc.b"); + const lj = B.newLabel("oc.j"); + B.condBr(isUnit, lj, lb); + B.startBlock(lb); + bindPresent(); + host.frames.push([]); + host.emitExpr(e.body); + host.releaseTemps(host.frames.pop()!); + host.chainSlots.delete(e.id); + B.br(lj); + B.startBlock(lj); + return { name: "", type: e.type }; + } + if (e.type.kind === "dyn") { + // A dyn-typed chain (`pricing?.[key]` over an unknown-valued + // index signature): the unit path is the undefined dyn value — + // dyn represents undefined directly, no union wrapper exists. + const slotD = B.slot(); + B.entryAllocas.push(`${slotD} = alloca ptr`); + const lu = B.newLabel("ocu.u"); + const lb = B.newLabel("ocu.b"); + const lj = B.newLabel("ocu.j"); + B.condBr(isUnit, lu, lb); + B.startBlock(lu); + host.declare(`declare ptr @scr_dyn_undefined()`); + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + const un = B.tmp(); + const ur = B.tmp(); + B.line(`${un} = call ptr @scr_dyn_undefined()`); + B.line(`${ur} = call ptr @scr_dyn_retain_v(ptr ${un})`); + B.line(`store ptr ${ur}, ptr ${slotD}`); + B.br(lj); + B.startBlock(lb); + bindPresent(); + host.emitBranchInto(slotD, e.body); + host.chainSlots.delete(e.id); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${slotD}`); + return host.own({ name: t, type: e.type }); + } + if (e.type.kind !== "union") + throw new LlvmUnsupportedError(`optChainResult:${e.type.kind}`, e.loc); + const undefTag = undefinedArmTag(e.type, host.unionsById); + if (undefTag < 0) + throw new InternalCompilerError("llvm emitter bug: optChain result lacks its undefined arm"); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const lu = B.newLabel("oc.u"); + const lb = B.newLabel("oc.b"); + const lj = B.newLabel("oc.j"); + B.condBr(isUnit, lu, lb); + B.startBlock(lu); + B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`); + B.br(lj); + B.startBlock(lb); + bindPresent(); + host.emitBranchInto(slot, e.body); + host.chainSlots.delete(e.id); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); +} + +function emitChainRecv(host: LlvmEmitterContext, e: ExprOf<"chainRecv">): LlValue { + const B = host.B; + const bound = host.chainSlots.get(e.id); + if (!bound) + throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`); + const v = B.tmp(); + B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`); + if (!isRefCounted(e.type)) return { name: v, type: e.type }; + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); +} + +function emitOrDefault(host: LlvmEmitterContext, e: ExprOf<"orDefault">): LlValue { + const B = host.B; + // `u || d` narrowed to the single non-unit arm: nullish's dance + // with the union TRUTHY switch as the test — truthy extracts the + // arm (+1 for ref kinds), falsy releases and runs the default. + if (e.left.type.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: orDefault left is not a union"); + if ( + e.retag === undefined && + !host.nullableUnions.has(e.left.type.unionId) && + host.isStackUnionSource(e.left) + ) { + // A stack box (`arr[i] || d`): test truthiness and extract the + // payload (+1). The box is never retained or released. + const l = host.emitUnionProjection(e.left); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const lt = B.newLabel("ord.t"), + lf = B.newLabel("ord.f"), + lj = B.newLabel("ord.j"); + B.condBr(host.truthy(l), lt, lf); + B.startBlock(lt); + B.line(`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`); + B.br(lj); + B.startBlock(lf); + host.emitBranchInto(slot, e.right); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); + } + const l = host.emitExpr(e.left); + host.toFrame(l); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const truthy = host.truthy(l); + const lt = B.newLabel("ord.t"); + const lf = B.newLabel("ord.f"); + const lj = B.newLabel("ord.j"); + B.condBr(truthy, lt, lf); + B.startBlock(lt); + if (e.retag !== undefined) { + // Retagged shape: the whole box goes to the union→union helper, + // which CONSUMES it (callees own their params) — no release on + // this side. The store precedes the pending check so an unwind + // leaves only the NULL dummy behind, in a slot nothing reads. + const t = B.tmp(); + B.line(`${t} = call ${ty} @${host.callTarget(e.retag)}(${host.llType(e.left.type)} ${l.name})`); + B.line(`store ${ty} ${t}, ptr ${slot}`); + if (host.mayThrow.has(e.retag)) host.emitPendingCheck(); + } else if (host.nullableUnions.single(e.left.type.unionId)) { + // The owned nullable pointer IS the owned arm value. + B.line(`store ${ty} ${l.name}, ptr ${slot}`); + } else { + B.line(`store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`); + host.releaseFrame(l.name, e.left.type); + } + B.br(lj); + B.startBlock(lf); + host.releaseFrame(l.name, e.left.type); + host.emitBranchInto(slot, e.right); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); +} + +function emitNullish(host: LlvmEmitterContext, e: ExprOf<"nullish">): LlValue { + const B = host.B; + // `a ?? b`: logical's move/release dance with the left's runtime + // TAG against its unit arms as the test. Pass-through shape: the + // result IS the left box. Narrowed shape: the single non-unit + // arm's payload extracts (+1 for ref kinds) and the box releases. + + if (e.left.type.kind === "dyn") { + // The native checked-value kind decides whether the left is + // nullish; the right runs lazily in its branch. + const l = host.emitExpr(e.left); + host.toFrame(l); + host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ptr`); + const isN = B.tmp(); + B.line(`${isN} = call zeroext i1 @scr_dyn_is_nullish(ptr ${l.name})`); + const lu = B.newLabel("nuld.u"); + const lv = B.newLabel("nuld.v"); + const lj = B.newLabel("nuld.j"); + B.condBr(isN, lu, lv); + B.startBlock(lu); + host.releaseFrame(l.name, e.left.type); + host.emitBranchInto(slot, e.right); + B.br(lj); + B.startBlock(lv); + B.line(`store ptr ${l.name}, ptr ${slot}`); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ptr, ptr ${slot}`); + return host.own({ name: t, type: e.type }); + } + if (e.left.type.kind !== "union") + throw new InternalCompilerError("llvm emitter bug: nullish left is not a union"); + const def = host.unionsById.get(e.left.type.unionId); + if (!def) + throw new InternalCompilerError( + `llvm emitter bug: nullish of unknown union ${e.left.type.unionId}`, + ); + const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : [])); + if (unitTags.length === 0) + 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" || + host.isStackUnionSource(e.left) || + host.nullableFieldGet(e.left) !== null) + ) { + // Narrowed shape over a binding or a stack box (`arr[i] ?? d`, + // `map.get(k) ?? d`): test the tag and extract the payload (+1). + // The box is never retained or released. A binding is borrowed: + // no code runs between its read and the extraction, and the + // default never touches it. Bindings proven projection-only may + // hold stack boxes, which must never reach RC entry points. + const l = host.emitUnionProjection(e.left); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const isUnit = host.tagInSet(l.name, def.id, unitTags); + const lu = B.newLabel("nul.u"); + const lv = B.newLabel("nul.v"); + const lj = B.newLabel("nul.j"); + B.condBr(isUnit, lu, lv); + B.startBlock(lu); + host.emitBranchInto(slot, e.right); + B.br(lj); + B.startBlock(lv); + B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); + } + const l = host.emitExpr(e.left); + host.toFrame(l); + const ty = host.llType(e.type); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca ${ty}`); + const isUnit = host.tagInSet(l.name, def.id, unitTags); + const lu = B.newLabel("nul.u"); + const lv = B.newLabel("nul.v"); + const lj = B.newLabel("nul.j"); + B.condBr(isUnit, lu, lv); + B.startBlock(lu); + host.releaseFrame(l.name, e.left.type); + host.emitBranchInto(slot, e.right); + B.br(lj); + B.startBlock(lv); + if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) { + // Pass-through, or a nullable pointer that IS its owned arm value. + B.line(`store ${ty} ${l.name}, ptr ${slot}`); + } else { + B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); + host.releaseFrame(l.name, e.left.type); + } + B.br(lj); + B.startBlock(lj); + const t = B.tmp(); + B.line(`${t} = load ${ty}, ptr ${slot}`); + return host.own({ name: t, type: e.type }); +} + /** The receiver half of a union-receiver `a?.b`. The receiver evaluates * once; `isUnit` tests its unit arms, and `bindPresent` fills the chain's * bind slot with the narrowed payload for the body to read through diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 6e1c1ac4e..088d392d6 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -5,6 +5,7 @@ import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../i import { mangleRecordClone, mangleRecordNew } from "../mangle.js"; import { arrNewCall, elemAccess } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; +import { offsetIndex } from "./expr-bytes.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js"; @@ -46,7 +47,12 @@ import { slotWidth, type ArrayAccess, } from "./dense-array-access.js"; -import { emitWrappingSum, integerOrNaNRange, 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. */ @@ -205,7 +211,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. @@ -413,10 +431,15 @@ export function emitOperatorExpr( 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 (wrapped !== undefined || wideInteger) { + // The integer of an exact integer or NaN, for index and loop guards. + const nan = emitNanArithmetic(host, e, l, r, li, ri); + const offset = offsetIndex(e, l, r); + if (wrapped !== undefined || wideInteger || nan || offset) { const value: LlValue = { name: t, type: e.type }; + if (nan) Object.assign(value, nan); if (wrapped !== undefined) Object.assign(value, wrapped); if (wideInteger) value.wideInteger = wideInteger; + if (offset) value.offsetIndex = offset; return value; } } else { @@ -1060,16 +1083,21 @@ export function emitContainerExpr( 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.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true, host.arenaSite(e))}`); B.br(joined); B.startBlock(sizedLabel); - B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`); + B.line( + `${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity, false, host.arenaSite(e))}`, + ); 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)}`); + } else + B.line( + `${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small, host.arenaSite(e))}`, + ); } const out = host.own({ name: arr, type: e.type }); const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem); @@ -1342,7 +1370,7 @@ export function emitRecordExpr( throw new InternalCompilerError("llvm emitter bug: recordLit of non-record type"); const shapeId = e.type.shapeId; const rec = B.tmp(); - B.line(`${rec} = call ptr @${mangleRecordNew(shapeId)}()`); + B.line(`${rec} = call ptr @${host.allocSym(mangleRecordNew(shapeId), e)}()`); const out = host.own({ name: rec, type: e.type }); // A null-prototype record: SCR_MAP_NULL_PROTO on the fresh map. if (e.nullProto) B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(rec, shapeId)}`); diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index af4bb092f..58f1297b3 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -195,9 +195,31 @@ function emitReplaceWith( 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; diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts index 244b3afea..4649056e1 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts @@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => { const llvm = emitLlvmModule(module, { pointerBits: 64 }); const tables = [ ...llvm.matchAll( - /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm, + /^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr, ptr \} \{ i64 (\d+), /gm, ), ]; // root and leaf get field hooks; the record with an index-like field name diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index f711795d3..516c08076 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -1484,6 +1484,10 @@ function liveRecordOps( G.startBlock(bad); G.terminate(`unreachable`); + // Only a program that writes through a dynamic view needs the store + // hook (and its checked conversions), as for the commit adapter. + const setDefs = host.liveViewsReadOnly ? [] : liveRecordSet(host, t, `${snapshot}_set`, fields); + const literals = fields.map((field) => `ptr ${host.internLiteral(field.name)}`); host.resolveThunkDefs.push( `define internal zeroext i1 @${has}(ptr %p, ${host.sizeType} %i) ${FN_ATTRS} { ; live field presence of ${typeKey(t)}`, @@ -1494,13 +1498,90 @@ function liveRecordOps( G.render(), `}`, ``, + ...setDefs, `@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`, - // Mutable: the runtime fills the trailing key-array slot on first use. - `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`, + // Mutable: the runtime fills the key-array slot on first use. + `@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null, ptr ${setDefs.length ? `@${snapshot}_set` : "null"} }`, ``, ); } +/** The `set` hook of a live record's ops table: stores a dynamic write of + * present field `index` into the native record in place, so the capsule + * never becomes a snapshot for it. A value the field type does not accept + * answers false with no side effects, leaving the snapshot path to raise + * the commit's diagnostic; an inline union field always does. */ +function liveRecordSet( + host: LlvmEmitterContext, + t: IrType & { kind: "record" }, + sym: string, + fields: readonly IrRecordShape["fields"][number][], +): string[] { + const shape = host.recordsById.get(t.shapeId)!; + const struct = mangleRecordStruct(t.shapeId); + const S = new BlockBuilder(); + const labels = fields.map(() => S.newLabel("live.record.store")); + const refuse = S.newLabel("live.record.refuse"); + S.terminate( + `switch ${host.sizeType} %i, label %${refuse} [ ${fields + .map((_, position) => `${host.sizeType} ${position}, label %${labels[position]}`) + .join(" ")} ]`, + ); + fields.forEach((field, position) => { + S.startBlock(labels[position]!); + const inline = host.inlineUnions.record(field.type); + if (inline) { + S.br(refuse); + return; + } + const fits = S.tmp(); + S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(field.type)}(ptr %v)`); + const check = S.newLabel("live.record.check"); + S.condBr(fits, check, refuse); + S.startBlock(check); + const valueTy = host.llType(field.type); + const next = S.tmp(); + S.line( + `${next} = call ${valueTy === "i1" ? "zeroext " : ""}${valueTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %v, ptr null)`, + ); + // A conversion that throws has already raised the write's error. + const pending = S.tmp(); + for (const line of host.pendingTestLines(pending)) S.line(line.trimStart()); + const write = S.newLabel("live.record.write"); + const done = S.newLabel("live.record.done"); + S.condBr(pending, done, write); + S.startBlock(write); + const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId); + const fieldPtr = S.tmp(); + S.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`); + const fieldTy = llFieldType(field.type, false); + let stored = next; + if (fieldTy === "i8") { + stored = S.tmp(); + S.line(`${stored} = zext i1 ${next} to i8`); + } + if (isRefCounted(field.type)) { + const old = S.tmp(); + S.line(`${old} = load ptr, ptr ${fieldPtr}`); + S.line(`store ptr ${stored}, ptr ${fieldPtr} ; takes ownership`); + S.line(`call void ${releaseSym(host.shapeHost, field.type)}(ptr ${old})`); + } else { + S.line(`store ${fieldTy} ${stored}, ptr ${fieldPtr}`); + } + S.br(done); + S.startBlock(done); + S.terminate(`ret i1 true`); + }); + S.startBlock(refuse); + S.terminate(`ret i1 false`); + return [ + `define internal zeroext i1 @${sym}(ptr %p, ${host.sizeType} %i, ptr %v) ${FN_ATTRS} { ; live field store into ${typeKey(t)}`, + S.render(), + `}`, + ``, + ]; +} + /** `@_rops` for a shape with orderVariants: a record-ops table * whose `select` hook answers the `_o_rops` table (liveRecordOps) of the * record's current key order, which a write of an optional key can change @@ -1524,7 +1605,7 @@ function liveRecordOrderSelect( B.render(), `}`, ``, - `@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`, + `@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select, ptr null }`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 285f9ca9a..3d9d8da76 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -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/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/heap-census.ts b/packages/compiler/src/backend/llvm/heap-census.ts new file mode 100644 index 000000000..b55e9341b --- /dev/null +++ b/packages/compiler/src/backend/llvm/heap-census.ts @@ -0,0 +1,241 @@ +/* The heap census. A program built with SCRIPTC_HEAP_CENSUS set counts, for + * ranking representation work: + * - the instances of each class that `new` or a class value constructs, + * with the class's size; + * - for each array-typed class field, its stores (with the stored array's + * length in buckets and the total of those lengths), the stores whose + * value is an array literal written at the store, and its accesses; + * - for each record-typed class field, its stores and accesses. + * Accesses count every field address the backend computes for the field, + * so a store is also an access. At exit the program writes one + * tab-separated line per counter to the file the run's SCRIPTC_HEAP_CENSUS + * names, or to stderr. Linux only: the runtime calls the writer through a + * weak reference, as it does the profile writer. Counters are module + * globals updated with monotonic atomic adds, so they include every + * thread's work. Ordinary builds emit none of it. */ +import type { IrClassDef, IrExpr, IrModule, IrType } from "../../ir/ir.js"; +import { classStructSym } from "./classes.js"; +import { llvmBytes, wtf8Bytes } from "../literals.js"; +import type { NullableUnions } from "./nullable-unions.js"; + +/** Whether this build counts the heap census: SCRIPTC_HEAP_CENSUS is set + * and nonempty. The executable cache keys on it. */ +export function heapCensusRequested(env: NodeJS.ProcessEnv = process.env): boolean { + const flag = env["SCRIPTC_HEAP_CENSUS"]; + return flag !== undefined && flag !== ""; +} + +/** The census of the module being built, or null when it is off. */ +export function heapCensusOf(mod: IrModule, unions: NullableUnions): HeapCensus | null { + if (!heapCensusRequested() || mod.lib !== undefined) return null; + return new HeapCensus(mod, unions); +} + +/** A NUL-terminated i8 array constant of `text`. */ +function cString(text: string): string { + const bytes = wtf8Bytes(text); + return `[${bytes.length + 1} x i8] c"${llvmBytes(bytes)}"`; +} + +const LENGTH_BUCKETS = ["len0", "len1", "len2", "len3-4", "len5-8", "len9-16", "len17+"]; +/** Upper bounds of LENGTH_BUCKETS but the last. */ +const BUCKET_LIMITS = [0, 1, 2, 4, 8, 16]; + +interface FieldCounters { + /** First counter: stores, accesses, then (arrays) the literal stores, + * the length total and the buckets. */ + base: number; + array: boolean; + /** The stored value is a pointer that is NULL for the union's unit arm. */ + nullable: boolean; +} + +export class HeapCensus { + private readonly names: string[] = []; + private readonly sizes: (string | null)[] = []; + private readonly instances = new Map(); + private readonly fields = new Map(); + + constructor(mod: IrModule, unions: NullableUnions) { + for (const cls of mod.classes ?? []) { + this.instances.set(cls.name, this.add(`new\t${cls.name}\t`, classStructSym(cls.name))); + for (const field of cls.fields) this.addField(cls, field.name, field.type, unions); + } + } + + /** Counts one instance of `className`. */ + countNew(line: (text: string) => void, className: string): void { + for (const text of this.newLines(className)) line(text); + } + + /** The lines that count one instance of `className`: a `new`, or a + * construct thunk for class values (`newValue`). */ + newLines(className: string): string[] { + const index = this.instances.get(className); + return index === undefined ? [] : [this.increment(index, "1")]; + } + + /** Counts an access of a field; true when the field is counted. */ + countAccess(line: (text: string) => void, className: string, field: string): void { + const counters = this.fields.get(`${className}\0${field}`); + if (counters !== undefined) line(this.increment(counters.base + 1, "1")); + } + + /** Counts a store of `value` (an LLVM pointer, computed from `source`) + * into a field: its length when the field holds arrays. `tmp` names fresh + * temporaries. */ + countStore( + line: (text: string) => void, + tmp: () => string, + className: string, + field: string, + source: IrExpr, + value: string | null, + ): void { + const counters = this.fields.get(`${className}\0${field}`); + if (counters === undefined) return; + line(this.increment(counters.base, "1")); + if (!counters.array) return; + const literal = source.kind === "unionWrap" ? source.value : source; + if (literal.kind === "arrayLit") line(this.increment(counters.base + 2, "1")); + if (value === null) return; + // A NULL pointer (the unit arm) counts as length 0: its length is read + // through a select that points NULL at a zeroed array header. + let array = value; + if (counters.nullable) { + const isNull = tmp(); + array = tmp(); + line(`${isNull} = icmp eq ptr ${value}, null`); + line(`${array} = select i1 ${isNull}, ptr @sc_census_zero_arr, ptr ${value}`); + } + const lenPtr = tmp(), + len32 = tmp(), + len = tmp(); + line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`); + line(`${len32} = load i32, ptr ${lenPtr}`); + line(`${len} = zext i32 ${len32} to i64`); + line(this.increment(counters.base + 3, len)); + // The bucket: the number of limits below the length. + let bucket = "0"; + for (const limit of BUCKET_LIMITS) { + const above = tmp(), + step = tmp(), + next = tmp(); + line(`${above} = icmp ugt i64 ${len}, ${limit}`); + line(`${step} = zext i1 ${above} to i64`); + line(`${next} = add i64 ${bucket}, ${step}`); + bucket = next; + } + const slot = tmp(); + line(`${slot} = add i64 ${bucket}, ${counters.base + 4}`); + const ptr = tmp(); + line( + `${ptr} = getelementptr inbounds [${this.names.length} x i64], ptr @sc_census, i64 0, i64 ${slot}`, + ); + line(`atomicrmw add ptr ${ptr}, i64 1 monotonic`); + } + + /** The module definitions: the counters, their names, the zero array + * header NULL stores read their length from, and the writer the runtime + * calls at exit (scr_census_write). */ + definitions(): string[] { + const n = this.names.length; + const out = [`@sc_census = internal global [${n} x i64] zeroinitializer`]; + // A zeroed %ScrArr: length 0. + out.push(`@sc_census_zero_arr = internal constant %ScrArr zeroinitializer`); + const names = this.names.map((name, i) => { + const bytes = cString(name); + out.push(`@sc_census_name.${i} = private unnamed_addr constant ${bytes}`); + return `ptr @sc_census_name.${i}`; + }); + out.push(`@sc_census_names = internal constant [${n} x ptr] [${names.join(", ")}]`); + const sizes = this.sizes.map((sym) => + sym === null ? "i64 0" : `i64 ptrtoint (ptr getelementptr (%${sym}, ptr null, i32 1) to i64)`, + ); + out.push(`@sc_census_sizes = internal constant [${n} x i64] [${sizes.join(", ")}]`); + const env = cString("SCRIPTC_HEAP_CENSUS"); + out.push(`@sc_census_env = private unnamed_addr constant ${env}`); + out.push(`@sc_census_mode = private unnamed_addr constant ${cString("w")}`); + out.push(`@sc_census_fmt = private unnamed_addr constant ${cString("%s\t%llu\t%llu\n")}`); + out.push( + `define void @scr_census_write() {`, + `entry:`, + ` %path = call ptr @getenv(ptr @sc_census_env)`, + ` %named = icmp ne ptr %path, null`, + ` br i1 %named, label %open, label %err`, + `open:`, + ` %file = call ptr @fopen(ptr %path, ptr @sc_census_mode)`, + ` %opened = icmp ne ptr %file, null`, + ` br i1 %opened, label %write, label %err`, + `err:`, + ` %stderr = load ptr, ptr @stderr`, + ` br label %write`, + `write:`, + ` %out = phi ptr [ %file, %open ], [ %stderr, %err ]`, + ` br label %loop`, + `loop:`, + ` %i = phi i64 [ 0, %write ], [ %next, %loop ]`, + ` %namep = getelementptr inbounds [${n} x ptr], ptr @sc_census_names, i64 0, i64 %i`, + ` %name = load ptr, ptr %namep`, + ` %countp = getelementptr inbounds [${n} x i64], ptr @sc_census, i64 0, i64 %i`, + ` %count = load atomic i64, ptr %countp monotonic, align 8`, + ` %sizep = getelementptr inbounds [${n} x i64], ptr @sc_census_sizes, i64 0, i64 %i`, + ` %size = load i64, ptr %sizep`, + ` %bytes = mul i64 %count, %size`, + ` call i32 (ptr, ptr, ...) @fprintf(ptr %out, ptr @sc_census_fmt, ptr %name, i64 %count, i64 %bytes)`, + ` %next = add i64 %i, 1`, + ` %done = icmp eq i64 %next, ${n}`, + ` br i1 %done, label %end, label %loop`, + `end:`, + ` call i32 @fflush(ptr %out)`, + ` ret void`, + `}`, + ); + return out; + } + + /** External declarations the writer uses. */ + declarations(): string[] { + return [ + "declare ptr @getenv(ptr)", + "declare ptr @fopen(ptr, ptr)", + "declare i32 @fprintf(ptr, ptr, ...)", + "declare i32 @fflush(ptr)", + "@stderr = external global ptr", + ]; + } + + private add(name: string, sizeSym: string | null = null): number { + this.names.push(name); + this.sizes.push(sizeSym); + return this.names.length - 1; + } + + private addField(cls: IrClassDef, field: string, type: IrType, unions: NullableUnions): void { + let array = type.kind === "array"; + let nullable = false; + let record = type.kind === "record"; + if (type.kind === "union") { + const single = unions.single(type.unionId); + if (single !== null && single.nullTag < 0) { + array = single.arm.kind === "array"; + record = single.arm.kind === "record"; + nullable = true; + } + } + if (!array && !record) return; + const key = `${cls.name}\t${field}`; + const base = this.add(`store\t${key}`); + this.add(`access\t${key}`); + if (array) { + this.add(`literal\t${key}`); + this.add(`elements\t${key}`); + for (const bucket of LENGTH_BUCKETS) this.add(`${bucket}\t${key}`); + } + this.fields.set(`${cls.name}\0${field}`, { base, array, nullable }); + } + + private increment(index: number, by: string): string { + return `atomicrmw add ptr getelementptr inbounds ([${this.names.length} x i64], ptr @sc_census, i64 0, i64 ${index}), i64 ${by} monotonic`; + } +} diff --git a/packages/compiler/src/backend/llvm/int32-kind-loops.ts b/packages/compiler/src/backend/llvm/int32-kind-loops.ts index 12e38c1f0..d596a731d 100644 --- a/packages/compiler/src/backend/llvm/int32-kind-loops.ts +++ b/packages/compiler/src/backend/llvm/int32-kind-loops.ts @@ -57,6 +57,8 @@ function isPure(e: IrExpr): boolean { case "arrayGet": case "arrayHas": return e.arr.kind === "varRef" && isPure(e.index); + case "arrIntrinsic": + return e.method === "getNumber" && e.receiver.kind === "varRef" && isPure(e.args[0]!); default: return false; } diff --git a/packages/compiler/src/backend/llvm/integer-clones.ts b/packages/compiler/src/backend/llvm/integer-clones.ts index 314eae5ea..0ea3947b2 100644 --- a/packages/compiler/src/backend/llvm/integer-clones.ts +++ b/packages/compiler/src/backend/llvm/integer-clones.ts @@ -3,6 +3,7 @@ import type { IrExpr, IrFunction, IrLocal, IrModule, IrStmt, IrType } from "../. import { mangleFunction } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; import { SCR_ARR_FIELD } from "./common.js"; +import { SCR_ELEM_F64, SCR_ELEM_I32 } from "./dense-array-access.js"; /** Speculative integer clones of pure number-valued functions. * @@ -191,6 +192,8 @@ function cloneShape(fn: IrFunction, borrowed?: BorrowedParameter): Set | case "ternary": return expr(e.cond, "bool") && expr(e.then, want) && expr(e.else_, want); case "arrIntrinsic": + if (e.method === "getNumber") + return want === "num" && arrayRef(e.receiver) && expr(e.args[0]!, "num"); return ( want === "num" && e.method === "length" && e.args.length === 0 && arrayRef(e.receiver) ); @@ -268,6 +271,9 @@ function callsMatch(fn: IrFunction, byName: ReadonlyMap): bo case "arrayGet": visit(e.index); return; + case "arrIntrinsic": + if (e.method === "getNumber") visit(e.args[0]!); + return; default: return; } @@ -432,6 +438,8 @@ function callsAnything(e: IrExpr): boolean { return callsAnything(e.cond) || callsAnything(e.then) || callsAnything(e.else_); case "arrayGet": return callsAnything(e.index); + case "arrIntrinsic": + return e.method === "getNumber" && callsAnything(e.args[0]!); default: return false; } @@ -1020,10 +1028,10 @@ class CloneEmitter { * keeps every slot below `filled` a present value) whose element is * exactly an int32. Everything else (holes, indices outside the prefix, * other numbers) bails: the ordinary body reads it. */ - private emitElement(e: Extract): string { + private emitElement(arr: IrExpr, indexExpr: IrExpr): string { const { B } = this; - const array = this.emitArray(e.arr); - const index = this.narrow(this.emitNum(e.index)); + const array = this.emitArray(arr); + const index = this.narrow(this.emitNum(indexExpr)); const filledField = B.tmp(); const filled = B.tmp(); const inside = B.tmp(); @@ -1044,9 +1052,39 @@ class CloneEmitter { ); B.line(`${data} = load ptr, ptr ${dataField}`); B.line(`${offset} = zext i32 ${index} to i64`); + // A number array keeps 8-byte doubles, or 4-byte int32 slots when it is + // proven to hold only int32s (SCR_ELEM_I32); any other storage bails. + const kindField = B.tmp(); + const kind = B.tmp(); + const isInt32 = B.tmp(); + B.line( + `${kindField} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.elem}`, + ); + B.line(`${kind} = load i8, ptr ${kindField}`); + B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`); + const int32Slots = B.newLabel("elem.i32"); + const doubleSlots = B.newLabel("elem.f64"); + const join = B.newLabel("elem.join"); + B.condBr(isInt32, int32Slots, doubleSlots); + B.startBlock(int32Slots); + const int32Slot = B.tmp(); + const int32 = B.tmp(); + B.line(`${int32Slot} = getelementptr inbounds i32, ptr ${data}, i64 ${offset}`); + B.line(`${int32} = load i32, ptr ${int32Slot}`); + B.br(join); + B.startBlock(doubleSlots); + const isDouble = B.tmp(); + B.line(`${isDouble} = icmp eq i8 ${kind}, ${SCR_ELEM_F64}`); + this.require(isDouble); B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${offset}`); B.line(`${number} = load double, ptr ${slot}`); - return this.exactInt32(number); + const exact = this.exactInt32(number); + const doubleEnd = B.currentLabel(); + B.br(join); + B.startBlock(join); + const value = B.tmp(); + B.line(`${value} = phi i32 [ ${int32}, %${int32Slots} ], [ ${exact}, %${doubleEnd} ]`); + return value; } /** The int32 the double `number` equals bit for bit (never -0, NaN, a @@ -1157,9 +1195,13 @@ class CloneEmitter { if (this.inlines(e)) return { name: this.emitInlineCall(e), wide: false }; return { name: this.emitCall(e), wide: true }; case "arrIntrinsic": + // `getNumber` reads an element like `arrayGet`: a missing one bails, + // and the ordinary body reads it as NaN. + if (e.method === "getNumber") + return { name: this.emitElement(e.receiver, e.args[0]!), wide: false }; return { name: this.emitLength(this.emitArray(e.receiver)), wide: false }; case "arrayGet": - return { name: this.emitElement(e), wide: false }; + return { name: this.emitElement(e.arr, e.index), wide: false }; case "unary": { const operand = this.narrow(this.emitNum(e.operand)); const value = B.tmp(); 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 d51b838bb..c9786c9f2 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -4,13 +4,12 @@ * A property site in a dynamic-tier function owns one zeroed thread-local * ScrJsIc record (`@sc_js_ic_N`). The code at the site is fixed: * - * load ordinary object whose shape is entry 0's own-property shape: - * read entry `slot` inline unless it is an accessor; otherwise - * 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) + * load ordinary object whose layout is entry 0's own-property layout: + * read slot `slot` inline; otherwise scr_js_get_probe + * (polymorphic, prototype hits, absent keys, misses) + * store ordinary object whose layout is entry 0's replace layout, + * outside an intact parse region: 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 @@ -22,16 +21,19 @@ * defines the operation's semantics; nothing is ever patched. * * Layout facts (64-bit only; 32-bit targets keep the helper calls): - * ScrDyn kind +8; v.obj {len +16, entries +32, region +72, - * shape +80}; v.arr {len +16, items +32, presence +56}; - * v.num +16 - * ScrDynEntry 48 bytes: value +16, accessor bit 0 of +40, writable +41 - * ScrJsIc entries of 40 bytes {shape, target, holder, holder_shape, - * slot}; 168 bytes in all */ -import { BOOL, DYN, F64, STRING, type IrExpr, type IrFunction } from "../../ir/ir.js"; + * ScrDyn kind +8; v.obj {len +16, slots +32, region +72, + * layout +80}; v.arr {len +16, items +32, presence +56}; + * v.num +16. Every property of an object with a layout is + * a writable data property (scr_dyn.h's layout mode); + * dictionary-mode objects have a null layout. + * v.typed_ref {ptr +16, type_key +40}. + * ScrJsIc entries of 40 bytes {layout, target, holder, + * holder_layout, slot}; 168 bytes in all */ +import { BOOL, DYN, F64, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; import { jsIcMethodSites, type JsIcSite } from "../../ir/js-ic.js"; import { mangleBorrowedFunction } from "../mangle.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; +import { classFieldBase, classFieldIndex } from "./classes.js"; import { DYN_KIND } from "./dyn.js"; import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { JS_BITS } from "./jsval-encoding.js"; @@ -42,8 +44,6 @@ type Host = LlvmEmitterContext; /** Words of one ScrJsIc record. */ export const JS_IC_WORDS = 21; -const ENTRY_STRIDE = 48; - /** The cache site a call is, when it is emitted in a dynamic-tier function * of a 64-bit target. */ export function jsIcSiteOf(host: Host, e: ExprOf<"call">): JsIcSite | null { @@ -86,34 +86,22 @@ function kindIs(host: Host, d: string, kind: number, yes: string, no: string): v * inline made polymorphic reads 7-10% faster (the design prototype's W2/W3) * and monomorphic method code no faster, for 65-225 more bytes per site; * the prototype hit made method-heavy code 3-6% faster for 15-90 bytes. - * So a site tests entry 0, own or prototype, and its probe does the rest. - * (0 makes every access probe: with a runtime built with SCR_JS_VERIFY, - * that checks every cached shape against its object.) */ -const INLINE_WAYS: number = 1; + * Two entries cover the common two-layout site (object literals of one + * record built in two key orders): on bench:runtime's dynamic-properties + * they made the run 13% faster than one entry, and 26% faster without its + * class instances; four were no faster than two. So a site tests entries + * 0 and 1, own or prototype, and its probe does the rest. (0 makes every access probe: with a runtime + * built with SCR_JS_VERIFY, that probes every access.) */ +const INLINE_WAYS: number = 2; const INLINE_PROTO = true; -/** The address of entry `slot` of object `d`'s entries. */ -function entryAt(host: Host, d: string, slot: string): string { +/** The address of slot `slot` of layout-mode object `d`. */ +function slotAt(host: Host, d: string, slot: string): string { const B = host.B; - const entries = load(host, "ptr", d, 32); - const offset = B.tmp(); - const entry = B.tmp(); - B.line(`${offset} = mul i64 ${slot}, ${ENTRY_STRIDE}`); - B.line(`${entry} = getelementptr inbounds i8, ptr ${entries}, i64 ${offset}`); - return entry; -} - -/** Branches to `miss` when the entry at `entry` is an accessor. */ -function dataEntry(host: Host, entry: string, miss: string): void { - const B = host.B; - const flags = load(host, "i8", entry, 40); - const accessor = B.tmp(); - const isAccessor = B.tmp(); - B.line(`${accessor} = and i8 ${flags}, 1`); - B.line(`${isAccessor} = icmp ne i8 ${accessor}, 0`); - const data = B.newLabel("ic.data"); - B.condBr(isAccessor, miss, data); - B.startBlock(data); + const slots = load(host, "ptr", d, 32); + const address = B.tmp(); + B.line(`${address} = getelementptr inbounds ptr, ptr ${slots}, i64 ${slot}`); + return address; } /** The inline part of a load site, entered for an ordinary object `d`: @@ -137,7 +125,7 @@ function inlineLoad( B.br(miss); return; } - const shape = load(host, "ptr", d, 80); + const shape = load(host, "ptr", d, 80); // v.obj.layout const present = B.tmp(); B.line(`${present} = icmp ne ptr ${shape}, null`); let next = B.newLabel("ic.w"); @@ -160,9 +148,7 @@ function inlineLoad( B.condBr(own, ownLabel, protoLabel); const hit = (holder: string): void => { const slot = load(host, "i64", ic, base + 32); - const entry = entryAt(host, holder, slot); - dataEntry(host, entry, miss); - const value = load(host, "ptr", entry, 16); + const value = load(host, "ptr", slotAt(host, holder, slot), 0); const out = result(value); incoming.push(`[ ${out}, %${B.currentLabel()} ]`); B.br(join); @@ -194,7 +180,7 @@ function inlineLoad( /** The inline part of a store site that replaces an own data property, * entered for an ordinary object `d` outside an intact parse region: * tests the first `ways` cache entries; a hit calls `store` with the - * address of the entry's value word, which must end its block by + * address of the property's slot, which must end its block by * branching to the site's join. */ function inlineReplace( host: Host, @@ -209,7 +195,7 @@ function inlineReplace( B.br(miss); return; } - const shape = load(host, "ptr", d, 80); + const shape = load(host, "ptr", d, 80); // v.obj.layout const present = B.tmp(); B.line(`${present} = icmp ne ptr ${shape}, null`); let next = B.newLabel("ic.w"); @@ -230,21 +216,7 @@ function inlineReplace( B.condBr(ok, matched, next); B.startBlock(matched); const slot = load(host, "i64", ic, base + 32); - const entry = entryAt(host, d, slot); - const flags = load(host, "i8", entry, 40); - const writable = load(host, "i8", entry, 41); - const accessor = B.tmp(); - const data = B.tmp(); - const canWrite = B.tmp(); - const writes = B.tmp(); - B.line(`${accessor} = and i8 ${flags}, 1`); - B.line(`${data} = icmp eq i8 ${accessor}, 0`); - B.line(`${canWrite} = icmp ne i8 ${writable}, 0`); - B.line(`${writes} = and i1 ${data}, ${canWrite}`); - const hit = B.newLabel("ic.st"); - B.condBr(writes, hit, miss); - B.startBlock(hit); - store(gep(host, entry, 16)); + store(slotAt(host, d, slot)); } } @@ -347,6 +319,222 @@ function keyString(host: Host, key: string, e: IrExpr): string { return host.emitExpr({ kind: "strLit", value: key, type: STRING, loc: e.loc }).name; } +/** A class instance's answer that a read helper gives by loading one + * declared field: the branch `if (dyn.typedRefIs(p, KEY)) { r = + * dynCheck(p); return [js.fromDyn](fieldGet C.f(r)) }`. */ +interface CapsuleRead { + key: string; + className: string; + field: string; +} + +/** The class branches of a read helper that a site can answer inline for + * capsules (class instances boxed as dyn values), or none. + * + * The helper first normalizes its receiver (`%dyn.class.normalize` + * reboxes a capsule keyed with a base class whose instance belongs to a + * subclass), then tests each class branch in turn. The site loads the + * field of a capsule whose key is a branch's key without normalizing, so + * a branch qualifies only when every subclass the normalization can + * rebox its key to has a branch that loads the same field from the same + * slot of a layout that extends the class's. Every other capsule, and + * every helper or normalization of another form, keeps the helper. */ +function capsuleReads(host: Host, helper: IrFunction): CapsuleRead[] { + const receiver = helper.params[0]?.localId; + if (receiver === undefined || helper.params.length !== 1) return []; + const reads = new Map(); + for (const stmt of helper.body) { + if (normalizes(stmt, receiver)) continue; + if (stmt.kind !== "if" || stmt.else_ !== null) break; + if (notCapsule(stmt.cond, receiver)) continue; + const key = brandKey(stmt.cond, receiver); + if (key === null) return []; + if (!reads.has(key)) reads.set(key, capsuleRead(stmt, receiver, key)); + } + const reboxes = normalizationTargets(host); + if (reboxes === null) return []; + const out: CapsuleRead[] = []; + for (const read of reads.values()) { + if (read === null || !plainField(host, read.className, read.field)) continue; + const targets = reboxes.get(read.key) ?? []; + const same = targets.every((target) => { + const other = reads.get(`object:${target}`); + return ( + other != null && + other.field === read.field && + plainField(host, target, read.field) && + extendsLayout(host, target, read.className, read.field) + ); + }); + if (same) out.push(read); + } + return out; +} + +/** `p = %dyn.class.normalize(p)`. */ +function normalizes(stmt: IrStmt, receiver: string): boolean { + return ( + stmt.kind === "assign" && + stmt.localId === receiver && + stmt.value.kind === "call" && + stmt.value.callee === "%dyn.class.normalize" && + stmt.value.args.length === 1 && + isLocal(stmt.value.args[0]!, receiver) + ); +} + +/** `!classCapsule(p)`: false for every capsule of a compiled class, so a + * capsule never enters the branch it guards. */ +function notCapsule(cond: IrExpr, receiver: string): boolean { + return ( + cond.kind === "dynTest" && + cond.test === "classCapsule" && + cond.negated === true && + isLocal(cond.value, receiver) + ); +} + +function isLocal(e: IrExpr, id: string): boolean { + return e.kind === "varRef" && e.localId === id; +} + +/** The capsule key `dyn.typedRefIs(p, "object:C")` tests, or null. */ +function brandKey(cond: IrExpr, receiver: string): string | null { + if (cond.kind !== "libCall" || cond.fn !== "dyn.typedRefIs" || cond.args.length !== 2) + return null; + const [value, key] = cond.args; + if (!isLocal(value!, receiver) || key?.kind !== "strLit" || !key.value.startsWith("object:")) + return null; + return key.value; +} + +/** The field a class branch loads, or null for a branch of another form. */ +function capsuleRead( + branch: IrStmt & { kind: "if" }, + receiver: string, + key: string, +): CapsuleRead | null { + const [decl, ret] = branch.then; + if (branch.then.length !== 2 || decl?.kind !== "varDecl" || ret?.kind !== "return") return null; + if (decl.init?.kind !== "dynCheck" || !isLocal(decl.init.value, receiver)) return null; + let value = ret.value; + if (value?.kind === "libCall" && value.fn === "js.fromDyn" && value.args.length === 1) + value = value.args[0]!; + if (value?.kind !== "fieldGet" || !isLocal(value.obj, decl.localId)) return null; + if (key !== `object:${value.className}`) return null; + return { key, className: value.className, field: value.field }; +} + +/** For each capsule key `%dyn.class.normalize` reboxes, the classes it + * may rebox to; null when the normalization has another form. */ +function normalizationTargets(host: Host): Map | null { + const targets = new Map(); + const normalize = host.fnByName.get("%dyn.class.normalize"); + if (normalize === undefined) return targets; + const value = normalize.params[0]?.localId; + if (value === undefined) return null; + for (const stmt of normalize.body) { + if (stmt.kind === "return") { + if (stmt.value !== null && isLocal(stmt.value, value)) continue; + return null; + } + if (stmt.kind !== "if" || stmt.else_ !== null) return null; + if (notCapsule(stmt.cond, value)) continue; + const key = brandKey(stmt.cond, value); + if (key === null) return null; + const [decl, ...checks] = stmt.then; + if (decl?.kind !== "varDecl" || decl.init?.kind !== "dynCheck") return null; + const into = targets.get(key) ?? []; + for (const check of checks) { + if ( + check.kind !== "if" || + check.else_ !== null || + check.cond.kind !== "instanceOf" || + !isLocal(check.cond.value, decl.localId) + ) + return null; + into.push(check.cond.className); + } + targets.set(key, into); + } + return targets; +} + +/** A declared field of a compiled class that holds a plain dyn cell + * pointer: not compact, compressed, int32 or an inline union. */ +function plainField(host: Host, className: string, field: string): boolean { + const meta = host.classMeta.get(className); + if (meta === undefined || meta.compact) return false; + const decl = meta.def.fields.find((f) => f.name === field); + if (decl === undefined || decl.type.kind !== "dyn") return false; + if (classFieldIndex(meta, field, "compressed").compressed) return false; + return ( + !host.int32Slots.isField(className, field) && !host.inlineUnions.classField(className, field) + ); +} + +/** Whether `sub`'s layout starts with `base`'s through `field`: the same + * header and the same fields in the same slots, so a load through + * `base`'s struct reads `sub`'s field. */ +function extendsLayout(host: Host, sub: string, base: string, field: string): boolean { + const a = host.classMeta.get(base); + const b = host.classMeta.get(sub); + if (a === undefined || b === undefined || b.compact) return false; + if (classFieldBase(a) !== classFieldBase(b)) return false; + const end = a.def.fields.findIndex((f) => f.name === field); + if (end < 0 || end >= b.def.fields.length) return false; + for (let i = 0; i <= end; i++) { + const x = a.def.fields[i]!; + const y = b.def.fields[i]!; + if (x.name !== y.name || x.type.kind !== y.type.kind) return false; + } + return ( + classFieldIndex(a, field, "compressed").index === classFieldIndex(b, field, "compressed").index + ); +} + +/** The inline part of a read site for a capsule `d`: a capsule whose key + * is one of `reads`' keys answers with the (borrowed) field cell, branching + * to `join` and recording its phi input; anything else, including a field + * not yet set, branches to `miss`. */ +function inlineCapsuleLoad( + host: Host, + d: string, + reads: readonly CapsuleRead[], + incoming: string[], + join: string, + miss: string, +): void { + const B = host.B; + const capsule = B.newLabel("icc.cap"); + kindIs(host, d, DYN_KIND.TYPED_REF, capsule, miss); + B.startBlock(capsule); + // Capsules boxed by this program share its key constants. + const key = load(host, "ptr", d, 40); + let next = B.newLabel("icc.k"); + B.br(next); + reads.forEach((read, i) => { + B.startBlock(next); + next = i + 1 < reads.length ? B.newLabel("icc.k") : miss; + const same = B.tmp(); + B.line(`${same} = icmp eq ptr ${key}, ${host.cstr(read.key)}`); + const matched = B.newLabel("icc.m"); + B.condBr(same, matched, next); + B.startBlock(matched); + const instance = load(host, "ptr", d, 16); + const { ptr } = host.classFieldPtr(instance, read.className, read.field); + const cell = B.tmp(); + B.line(`${cell} = load ptr, ptr ${ptr}`); + const set = B.tmp(); + B.line(`${set} = icmp ne ptr ${cell}, null`); + const hit = B.newLabel("icc.hit"); + B.condBr(set, hit, miss); + B.startBlock(hit); + incoming.push(`[ ${cell}, %${B.currentLabel()} ]`); + B.br(join); + }); +} + /** Emits a call to a classified helper as a cache site. */ export function emitJsIcCall( host: Host, @@ -401,9 +589,15 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string) // conversion to the result type. const hit = B.newLabel("icg.hit"); const hits: string[] = []; - kindIs(host, receiver.name, DYN_KIND.OBJ, object, probe); + const reads = capsuleReads(host, callee); + const other = reads.length > 0 ? B.newLabel("icg.other") : probe; + kindIs(host, receiver.name, DYN_KIND.OBJ, object, other); B.startBlock(object); inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe); + if (reads.length > 0) { + B.startBlock(other); + inlineCapsuleLoad(host, receiver.name, reads, hits, hit, probe); + } B.startBlock(probe); // 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 diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index bd2e42c1e..c3acf766f 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"; @@ -217,7 +218,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll host.emitCoroAwait(null); return { name: "", type: e.type }; } - if (host.wasi && e.fn === "async.awaitDyn") { + if (host.coroAwait && e.fn === "async.awaitDyn") { const value = host.emitExpr(e.args[0]!); host.declare(`declare ptr @scr_dyn_promise_of(ptr)`); const promise = B.tmp(); @@ -229,15 +230,16 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`${isPromise} = icmp ne ptr ${promise}, null`); B.condBr(isPromise, wait, hop); B.startBlock(wait); - host.emitWasiSuspend(promise); + host.emitCoroAwait(promise); B.br(ready); B.startBlock(hop); - host.emitWasiSuspend(null); + host.emitCoroAwait(null); B.br(ready); B.startBlock(ready); - host.declare(`declare ptr @scr_await_dyn_value_settled(ptr)`); + const extract = host.wasi ? "scr_await_dyn_value_settled" : "scr_coro_await_dyn_value"; + host.declare(`declare ptr @${extract}(ptr)`); const result = B.tmp(); - B.line(`${result} = call ptr @scr_await_dyn_value_settled(ptr ${value.name})`); + B.line(`${result} = call ptr @${extract}(ptr ${value.name})`); const output = host.own({ name: result, type: e.type }); host.emitPendingCheck(); return output; @@ -278,6 +280,82 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll 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) @@ -678,7 +756,10 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // 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 === "dyn.keySet" || e.fn === "dyn.keySetComputed") && + host.sloppyDynWrites.has(`${e.loc.file}\0${e.loc.start}\0${e.loc.end}`) + ? `${LIB_FN_SYMS[e.fn]}_sloppy` + : 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, @@ -1155,6 +1236,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "punycode": case "constants": case "promise": + case "navigator": case "mime": return host.emitGenericLibCall(e); case "test": diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 63661ab4c..5136d4aa0 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -401,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-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 18f907c78..747c9c883 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -294,6 +294,12 @@ 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", @@ -423,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", @@ -1456,6 +1463,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", @@ -1715,6 +1729,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", @@ -1965,6 +1980,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/loop-callees.test.ts b/packages/compiler/src/backend/llvm/loop-callees.test.ts new file mode 100644 index 000000000..f918af944 --- /dev/null +++ b/packages/compiler/src/backend/llvm/loop-callees.test.ts @@ -0,0 +1,83 @@ +import { expect, test } from "vitest"; +import { + BOOL, + F64, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrStmt, +} from "../../ir/ir.js"; +import { findLoopCallees } from "./loop-callees.js"; + +const loc = { file: "callees.ts", start: 0, end: 0 }; +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const call = (callee: string): IrExpr => ({ kind: "call", callee, args: [num(1)], type: F64, loc }); +const run = (e: IrExpr): IrStmt => ({ kind: "exprStmt", expr: e, loc }); +const ret = (e: IrExpr): IrStmt => ({ kind: "return", value: e, loc }); +const loop = (body: IrStmt[]): IrStmt => ({ + kind: "while", + cond: { kind: "bin", op: "<", left: ref("i"), right: num(10), type: BOOL, loc }, + body, + loc, +}); +const fn = (name: string, body: IrStmt[], extra: Partial = {}): IrFunction => ({ + name, + params: [{ localId: "x", name: "x", type: F64 }], + returnType: F64, + locals: [{ id: "x", name: "x", type: F64, mutable: false }], + body, + loc, + ...extra, +}); +const module = (functions: IrFunction[]): IrModule => ({ + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + ...functions, + ], +}); + +test("a small leaf called only from loops is a loop callee", () => { + const callees = findLoopCallees( + module([ + fn("leaf", [loop([run(ref("x"))]), ret(ref("x"))]), + fn("caller", [loop([run(call("leaf"))]), loop([run(call("leaf"))]), ret(num(0))]), + ]), + ); + expect([...callees]).toEqual(["leaf"]); +}); + +test("a call outside a loop, a third call site, an own call or a large body keeps the call", () => { + const leaf = fn("leaf", [ret(ref("x"))]); + // A call site outside any loop. + expect( + findLoopCallees(module([leaf, fn("caller", [run(call("leaf")), loop([run(call("leaf"))])])])) + .size, + ).toBe(0); + // Three call sites. + const three = fn("caller", [loop([run(call("leaf")), run(call("leaf")), run(call("leaf"))])]); + expect(findLoopCallees(module([leaf, three])).size).toBe(0); + // A callee that calls another function. + const calling = fn("calling", [ret(call("leaf"))]); + const caller = fn("caller", [loop([run(call("calling"))])]); + // (leaf is then called outside a loop, from calling.) + expect(findLoopCallees(module([leaf, calling, caller])).size).toBe(0); + // Two loops, or more than 12 statements. + const twoLoops = fn("leaf", [loop([]), loop([]), ret(ref("x"))]); + expect(findLoopCallees(module([twoLoops, fn("caller", [loop([run(call("leaf"))])])])).size).toBe( + 0, + ); + const long = fn("leaf", [...Array.from({ length: 12 }, () => run(ref("x"))), ret(ref("x"))]); + expect(findLoopCallees(module([long, fn("caller", [loop([run(call("leaf"))])])])).size).toBe(0); + // Async functions and library modules. + const asyncLeaf = fn("leaf", [ret(ref("x"))], { async: true }); + expect(findLoopCallees(module([asyncLeaf, fn("caller", [loop([run(call("leaf"))])])])).size).toBe( + 0, + ); + const lib = { ...module([leaf, fn("caller", [loop([run(call("leaf"))])])]), lib: {} } as IrModule; + expect(findLoopCallees(lib).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/loop-callees.ts b/packages/compiler/src/backend/llvm/loop-callees.ts new file mode 100644 index 000000000..a3d767dae --- /dev/null +++ b/packages/compiler/src/backend/llvm/loop-callees.ts @@ -0,0 +1,110 @@ +import type { IrExpr, IrFunction, IrModule, IrStmt } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +/** Statements a callee may have, counting nested ones. */ +const MAX_STATEMENTS = 12; +/** Direct call sites a callee may have. */ +const MAX_CALL_SITES = 2; + +/** Expressions that run other code: a callee that has none calls nothing, + * so inlining it never inlines a call chain or a recursion. */ +const CALLS = new Set([ + "call", + "callValue", + "virtualCall", + "dynCall", + "dynInvoke", + "new", + "newValue", + "newPromise", + "ffiCall", + "awaitExpr", + "awaitUnionExpr", +]); + +function isLoop(s: IrStmt): boolean { + return s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf"; +} + +/** Small functions that are called only from inside loops: a per-element + * helper such as + * + * function pick(list: Gene[], rand: number): number { + * for (const gene of list) if (rand < gene.c) return gene.sc; + * return list[list.length - 1]!.sc; + * } + * + * called from a loop over the elements. After its loop versions and + * checks, such a function is often past LLVM's inline cost threshold, so + * every element pays a call, the spills around it, and checks that the + * caller's loop could hoist once inlined. + * + * A function qualifies when it is a synchronous, non-generator function + * with no captures and no own call (CALLS), has at most MAX_STATEMENTS + * statements and one loop, and has one to MAX_CALL_SITES direct call sites + * in the module, each inside a loop body of its caller. The emitter marks + * those calls `alwaysinline`. Functions the module exports as a library + * keep the cost model's decision. */ +export function findLoopCallees(mod: IrModule): Set { + const result = new Set(); + if (mod.lib !== undefined) return result; + const candidates = new Map(); + for (const fn of mod.functions) { + if (fn.async || fn.generator || fn.captures !== undefined || fn.classCaptures !== undefined) + continue; + let statements = 0; + let loops = 0; + let calls = false; + const expr = (e: IrExpr): boolean => { + if (CALLS.has(e.kind)) calls = true; + return !calls && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + statements++; + if (isLoop(s)) loops++; + return statements <= MAX_STATEMENTS && loops <= 1 && everyStmtChild(s, expr, stmt); + }; + if (!fn.body.every(stmt) || calls) continue; + candidates.set(fn.name, fn); + } + if (candidates.size === 0) return result; + // Call sites of each candidate, and whether every one is inside a loop. + const sites = new Map(); + for (const fn of mod.functions) { + let depth = 0; + const expr = (e: IrExpr): boolean => { + if (e.kind === "call" && candidates.has(e.callee)) { + const site = sites.get(e.callee) ?? { count: 0, inLoops: true }; + site.count++; + if (depth === 0 || e.callee === fn.name) site.inLoops = false; + sites.set(e.callee, site); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (!isLoop(s)) return everyStmtChild(s, expr, stmt); + // A loop's header runs per iteration too, except a for's init. + if (s.kind === "for" && s.init) stmt(s.init); + depth++; + if (s.kind === "for") { + if (s.cond) expr(s.cond); + if (s.update) stmt(s.update); + s.body.every(stmt); + } else if (s.kind === "forOf") { + depth--; + expr(s.iterable); + depth++; + s.body.every(stmt); + } else if (s.kind === "while" || s.kind === "doWhile") { + expr(s.cond); + s.body.every(stmt); + } + depth--; + return true; + }; + fn.body.every(stmt); + } + for (const [name, site] of sites) + if (site.inLoops && site.count >= 1 && site.count <= MAX_CALL_SITES) result.add(name); + return result; +} diff --git a/packages/compiler/src/backend/llvm/loop-entry-versions.ts b/packages/compiler/src/backend/llvm/loop-entry-versions.ts new file mode 100644 index 000000000..2c448cae4 --- /dev/null +++ b/packages/compiler/src/backend/llvm/loop-entry-versions.ts @@ -0,0 +1,231 @@ +/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while loop whose + * integer work depends on locals entering it without range facts runs as + * two copies. A test on entry checks that each such local holds an integer + * in its assumed range; the fast copy is emitted under the facts that hold + * then, and the other copy is the loop as it would be emitted otherwise. */ +import type { IrFunction, IrModule, IrStmt } from "../../ir/ir.js"; +import { everyStmt, everyStmtList } from "../../ir/traverse.js"; +import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js"; +import type { IntegerRangeFacts, IntegerSlotFacts } from "../../ir/integer-ranges.js"; +import { findLoopEntryVersions, type LoopEntryVersion } from "../../ir/loop-entry-ranges.js"; +import { f64Lit } from "./common.js"; +import type { LlEmitter } from "./emitter.js"; +import { defineModuleState } from "./emitter-state.js"; + +interface LoopEntryState { + fn: IrFunction | null; + facts: IntegerRangeFacts | null; + slots: IntegerSlotFacts; + /** Found on the first while loop the function emits. */ + versions: Map | null; + /** Loops whose copies are being emitted: a loop is versioned once. */ + readonly versioning: Set; + /** Direct calls per module function, or -1 for a function some value or + * the module's metadata references. */ + calls: Map | null; + /** The callees whose calls the fast copy being emitted inlines. */ + inlined: ReadonlySet | null; +} + +const ENTRY_VERSIONS = defineModuleState(() => ({ + fn: null, + facts: null, + slots: {}, + versions: null, + versioning: new Set(), + calls: null, + inlined: null, +})); + +/** The most IR nodes a callee may have for its call in a fast copy to be + * inlined (singleCallees). */ +const MAX_INLINED_NODES = 400; + +/** Sets the state for the function about to be emitted: `numericFn` and the + * facts emitFunction computed for it. Debug builds keep every loop as is. */ +export function beginLoopEntryVersions( + emitter: LlEmitter, + numericFn: IrFunction, + slots: IntegerSlotFacts, + facts: IntegerRangeFacts, +): void { + const state = ENTRY_VERSIONS.of(emitter); + state.fn = emitter.debug === null ? numericFn : null; + state.slots = slots; + state.facts = facts; + state.versions = null; + state.versioning.clear(); +} + +/** The entry version of `loop`, when it has one and the emitter can test + * its locals: plain double slots of the current function, outside a + * speculative copy and a coroutine. */ +export function loopEntryVersion(emitter: LlEmitter, loop: IrStmt): LoopEntryVersion | null { + if (loop.kind !== "while") return null; + const state = ENTRY_VERSIONS.of(emitter); + if (state.fn === null || state.facts === null || state.versioning.has(loop)) return null; + if ( + !emitter.speculativeLoopsEnabled || + emitter.speculation !== null || + emitter.speculationResume !== null || + emitter.currentCoro !== null + ) + return null; + state.versions ??= findLoopEntryVersions(state.fn, state.facts, state.slots); + const version = state.versions.get(loop); + if (!version) return null; + for (const id of version.entry.keys()) + if (!emitter.currentLocals.has(id) || !emitter.speculativeLocal(id)) return null; + return version; +} + +/** Emit the entry test and both copies of `loop`. `emitCopies` emits the + * loop as it would be emitted without the version; it runs once under the + * version's facts and once as is. */ +export function emitLoopEntryVersion( + emitter: LlEmitter, + loop: IrStmt, + version: LoopEntryVersion, + emitCopies: () => void, +): void { + const B = emitter.B; + let ok: string | null = null; + for (const [id, range] of version.entry) { + // An integer in [min, max] other than -0: the ordered comparisons + // reject NaN, and the bits of the value and its round trip agree only + // for integers that are not -0. A local the loop writes before it reads + // may be unset on entry, and a conversion out of range is poison: + // `freeze` makes both an arbitrary fixed value. + const raw = B.tmp(), + value = B.tmp(), + low = B.tmp(), + high = B.tmp(), + converted = B.tmp(), + integer = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + passed = B.tmp(); + B.line(`${raw} = load double, ptr ${emitter.binding(id).slot}`); + B.line(`${value} = freeze double ${raw}`); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(range.min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(range.max)}`); + B.line(`${converted} = fptosi double ${value} to i64`); + B.line(`${integer} = freeze i64 ${converted}`); + 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(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${passed} = and i1 ${inRange}, ${exact}`); + if (ok === null) ok = passed; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${passed}`); + ok = both; + } + } + const fast = B.newLabel("entry.fast"), + slow = B.newLabel("entry.slow"); + // The test is expected to pass, which keeps the ordinary copy's calls + // cold for the inliner. + let likely = "true"; + if (ok !== null) { + emitter.declare("declare i1 @llvm.expect.i1(i1, i1)"); + likely = B.tmp(); + B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${ok}, i1 true)`); + } + B.condBr(likely, fast, slow); + const state = ENTRY_VERSIONS.of(emitter); + const originalRanges = emitter.integerRanges; + const originalLoopRanges = emitter.integerLoopRanges; + state.versioning.add(loop); + try { + B.startBlock(fast); + // An enclosing version may have narrowed a range already. + const ranges = new Map(originalRanges); + for (const [node, range] of version.ranges) { + const known = originalRanges.get(node); + ranges.set( + node, + known + ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } + : range, + ); + } + emitter.integerRanges = ranges; + state.inlined = singleCallees(emitter, state, loop); + if (version.loopRanges) { + const loopRanges = new Map(originalLoopRanges); + loopRanges.set(loop, version.loopRanges); + emitter.integerLoopRanges = loopRanges; + } + emitCopies(); + state.inlined = null; + emitter.integerRanges = originalRanges; + emitter.integerLoopRanges = originalLoopRanges; + B.startBlock(slow); + emitCopies(); + } finally { + emitter.integerRanges = originalRanges; + emitter.integerLoopRanges = originalLoopRanges; + state.versioning.delete(loop); + state.inlined = null; + } +} + +/** Whether the fast copy being emitted inlines its calls of `callee`. */ +export function loopEntryInlines(emitter: LlEmitter, callee: string): boolean { + return ENTRY_VERSIONS.of(emitter).inlined?.has(callee) === true; +} + +/** The functions the loop calls that have no other call or reference in + * the module and at most MAX_INLINED_NODES nodes. LLVM inlines a function's + * only call at almost any size; the two copies give each such function a + * second call, so the fast copy's call is marked to keep that inlining. */ +function singleCallees(emitter: LlEmitter, state: LoopEntryState, loop: IrStmt): Set { + const callees = new Set(); + everyStmt(loop, { + expr: (e) => { + if (e.kind === "call") callees.add(e.callee); + return true; + }, + stmt: () => true, + }); + const result = new Set(); + if (callees.size === 0) return result; + state.calls ??= countCalls(emitter.mod); + for (const name of callees) { + if (state.calls.get(name) !== 1 || name === state.fn?.name) continue; + const fn = emitter.mod.functions.find((f) => f.name === name); + if (fn && !fn.async && !fn.generator && nodeCount(fn.body) <= MAX_INLINED_NODES) + result.add(name); + } + return result; +} + +function countCalls(mod: IrModule): Map { + const calls = new Map(); + const referenced = new Set(moduleFunctionReferences(mod)); + for (const fn of mod.functions) + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "call") calls.set(e.callee, (calls.get(e.callee) ?? 0) + 1); + const name = exprFunctionReference(e); + if (name !== undefined) referenced.add(name); + return true; + }, + stmt: () => true, + }); + for (const name of referenced) calls.set(name, -1); + return calls; +} + +function nodeCount(body: IrStmt[]): number { + let count = 0; + const visit = (): boolean => ++count <= MAX_INLINED_NODES; + everyStmtList(body, { expr: visit, stmt: visit }); + return count; +} 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/region-groups.test.ts b/packages/compiler/src/backend/llvm/region-groups.test.ts new file mode 100644 index 000000000..1e4e8dd0c --- /dev/null +++ b/packages/compiler/src/backend/llvm/region-groups.test.ts @@ -0,0 +1,205 @@ +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 { IrExpr, IrModule, IrType } from "../../ir/ir.js"; +import { analyzeClosedGroups, analyzeRegionGroups } from "./region-groups.js"; +import { children, type Node } from "./region-ir.js"; + +async function moduleFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-region-groups-")); + 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 }); + } +} + +/** The calls of `callee` in the module, in order. */ +function callsOf(mod: IrModule, callee: string): IrExpr[] { + const out: IrExpr[] = []; + const visit = (node: Node): void => { + if (node.kind === "call" && node.callee === callee) out.push(node); + const kids: Node[] = []; + children(node, kids); + for (const k of kids) visit(k); + }; + for (const fn of mod.functions) for (const s of fn.body) visit(s); + return out; +} + +// Every object and record is an arena type here, and every union a nullable +// pointer to one (the emitter decides both from layouts). +const everyObject = (t: IrType): boolean => + t.kind === "object" || t.kind === "record" || t.kind === "union"; + +const TREE = ` +class Node { + left: Node | null; + right: Node | null; + constructor(left: Node | null, right: Node | null) { + this.left = left; + this.right = right; + } + count(): number { + return 1 + (this.left === null ? 0 : this.left.count()) + (this.right === null ? 0 : this.right.count()); + } +} +function tree(depth: number): Node { + return depth === 0 ? new Node(null, null) : new Node(tree(depth - 1), tree(depth - 1)); +} +`; + +test("a builder whose result its caller only reads is a closed root", async () => { + const mod = await moduleFor(`${TREE} +let total = 0; +for (let d = 0; d < 4; d++) total += tree(d).count(); +console.log(total); +`); + const groups = analyzeRegionGroups(mod); + expect(groups.fresh.has("tree")).toBe(true); + const [root] = callsOf(mod, "tree").filter((c) => groups.roots.has(c)); + expect(root).toBeDefined(); + expect(groups.drops.get(root!)).toBeNull(); + expect(groups.reachable.has("tree")).toBe(true); + expect(analyzeClosedGroups(mod, groups, everyObject).has(root!)).toBe(true); +}); + +test("a result kept past its caller's frame is no root", async () => { + const mod = await moduleFor(`${TREE} +const kept: Node[] = []; +for (let d = 0; d < 4; d++) kept.push(tree(d)); +console.log(kept.length); +`); + const groups = analyzeRegionGroups(mod); + expect(callsOf(mod, "tree").some((c) => groups.roots.has(c))).toBe(false); +}); + +test("a function that stores what it allocates in a global is not leak-free", async () => { + const mod = await moduleFor(` +interface P { x: number } +const seen: P[] = []; +function note(x: number): void { + seen.push({ x }); +} +function sum(n: number): number { + let s = 0; + for (let i = 0; i < n; i++) { + const p = { x: i }; + s += p.x; + } + return s; +} +note(1); +console.log(sum(3), seen.length); +`); + const groups = analyzeRegionGroups(mod); + expect(groups.leakFree.has("note")).toBe(false); + expect(callsOf(mod, "note").some((c) => groups.calls.has(c))).toBe(false); + expect(groups.leakFree.has("sum")).toBe(true); +}); + +test("a leak-free call that returns no reference is a call region", async () => { + const mod = await moduleFor(` +interface V { x: number; y: number } +function add(a: V, b: V): V { + return { x: a.x + b.x, y: a.y + b.y }; +} +function walk(n: number): number { + let at: V = { x: 0, y: 0 }; + for (let i = 0; i < n; i++) at = add(at, { x: i, y: 1 }); + return at.x + at.y; +} +console.log(walk(10)); +`); + const groups = analyzeRegionGroups(mod); + const [region] = callsOf(mod, "walk"); + expect(groups.calls.has(region!)).toBe(true); +}); + +test("a graph that holds strings or outside objects is not closed", async () => { + const mod = await moduleFor(` +class Tag { + name: string; + constructor(name: string) { + this.name = name; + } +} +class Box { + tag: Tag; + owner: Tag | null; + constructor(tag: Tag, owner: Tag | null) { + this.tag = tag; + this.owner = owner; + } +} +const shared = new Tag("shared"); +function labelled(n: number): Box { + let box = new Box(new Tag("t" + n), null); + for (let i = 0; i < n; i++) box = new Box(new Tag("t" + i), null); + return box; +} +function owned(n: number): Box { + let box = new Box(new Tag("x"), shared); + for (let i = 0; i < n; i++) box = new Box(new Tag("x"), shared); + return box; +} +let n = 0; +for (let i = 0; i < 3; i++) n += labelled(i).tag.name.length + owned(i).tag.name.length; +console.log(n); +`); + const groups = analyzeRegionGroups(mod); + const closed = analyzeClosedGroups(mod, groups, everyObject); + const labelled = callsOf(mod, "labelled").filter((c) => groups.roots.has(c)); + const owned = callsOf(mod, "owned").filter((c) => groups.roots.has(c)); + expect(labelled.length).toBe(1); + expect(owned.length).toBe(1); + expect(closed.has(labelled[0]!)).toBe(false); + expect(closed.has(owned[0]!)).toBe(false); +}); + +test("a root bound in a loop body is dropped with that body's local", async () => { + const mod = await moduleFor(`${TREE} +function run(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + const t = tree(i % 4); + total += t.count(); + } + return total; +} +console.log(run(10)); +`); + const groups = analyzeRegionGroups(mod); + const [root] = callsOf(mod, "tree").filter((c) => groups.roots.has(c)); + const run = mod.functions.find((f) => f.name === "run")!; + const local = groups.drops.get(root!); + expect(run.locals.find((l) => l.id === local)?.name).toBe("t"); +}); + +test("a root whose value a callee writes into is not closed", async () => { + const mod = await moduleFor(`${TREE} +function graft(n: Node, extra: Node): void { + n.left = extra; +} +const spare = new Node(null, null); +let total = 0; +for (let d = 0; d < 3; d++) { + const t = tree(d); + graft(t, spare); + total += t.count(); +} +console.log(total); +`); + const groups = analyzeRegionGroups(mod); + const closed = analyzeClosedGroups(mod, groups, everyObject); + for (const root of callsOf(mod, "tree").filter((c) => groups.roots.has(c))) + expect(closed.has(root)).toBe(false); +}); diff --git a/packages/compiler/src/backend/llvm/region-groups.ts b/packages/compiler/src/backend/llvm/region-groups.ts new file mode 100644 index 000000000..ce8354c92 --- /dev/null +++ b/packages/compiler/src/backend/llvm/region-groups.ts @@ -0,0 +1,1697 @@ +/* Region groups: call sites whose result is a fresh object graph the + * caller only borrows and then drops, so everything the call allocates + * can live in an arena that is dropped at once (arenas.ts). + * + * The group of a call is every object allocated while it runs. It can be + * dropped with its result when nothing outside reaches any member once the + * call returns except through the result. A function is leak-free when + * every value it allocates (an allocation, a fresh-producing builtin, or + * the result of a call) dies in its frame, is returned, or is stored into + * the graph of a value it returns, and every function it calls is + * leak-free too. Members may refer out of the group (to longer-lived + * objects, which an arena would release when it drops); nothing outside + * may refer in. A function is fresh when it is leak-free and returns only + * values it allocated. + * + * A root is a call of a fresh function (or a leak-free construction) whose + * value, and every reference read out of it, the caller keeps in its frame: + * deep parameter summaries, where reading a reference out of a parameter + * makes a value the callee must keep too. Everything is solved + * optimistically over the call graph and refined to a fixpoint. */ +import { + isRefCounted, + type IrExpr, + type IrFunction, + type IrModule, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import { + ALLOCATIONS, + FRESH_ARRAY_METHODS, + argsOf, + children, + forEachNode, + indexOfNode, + isExpr, + keepsInFrame, + same, + refType, + transparent, + type Node, +} from "./region-ir.js"; +import { VirtualTargets } from "./reference-effects.js"; + +/** The module functions a virtual call can run, or null when unknown (or + * when `node` is no virtual call). */ +function virtualTargetsOf(vt: VirtualTargets, node: Node): readonly string[] | null { + return node.kind === "virtualCall" ? vt.targets(node.className, node.method) : null; +} + +export interface RegionGroups { + /** Fresh call and `new` expressions whose value the caller only borrows. */ + roots: Set; + /** Calls of leak-free functions that return no reference: everything + * they allocate is dead when they return. */ + calls: Set; + /** Where each root's arena is dropped: the local whose scope exit drops + * it, or null for the end of the statement. */ + drops: Map; + /** Functions whose allocations go to the open arena. */ + reachable: Set; + /** Roots and call regions whose groups are closed: every reference a + * member holds is to a member or an immortal value (analyzeClosedGroups + * fills it in). */ + closed: Set; + /** The locals that share a root's value in its function (empty for a + * value used in place), and the function of each root and call region. */ + rootLocals: Map>; + rootFunction: Map; + /** Functions whose results are fresh. */ + fresh: Set; + /** Functions that keep nothing they allocate except through their result. */ + leakFree: Set; + /** Functions that allocate, directly or through what they call. */ + allocates: Set; +} + +/** Projections whose result is a reference read out of the operand. + * Strings are leaves, not group members: reading one out of a member and + * keeping it anywhere keeps no member. */ +function projection(child: Node, parent: Node | undefined): boolean { + if (parent === undefined || !isExpr(parent) || !refType(parent.type)) return false; + switch (parent.kind) { + case "fieldGet": + case "recordGet": + case "recordKeyGet": + return same(parent.obj, child); + case "arrayGet": + return same(parent.arr, child); + case "unionKeyGet": + return same(parent.value, child); + case "mapIntrinsic": + case "setIntrinsic": + return ( + same(parent.receiver, child) && + (parent.method === "get" || parent.method === "iterKey" || parent.method === "iterValue") + ); + default: + return false; + } +} + +const LOOP_KINDS = new Set(["while", "doWhile", "for", "forOf"]); + +/** Collection methods that add their arguments to the receiver. */ +const ADDING_METHODS = new Set(["push", "unshift", "set", "add"]); + +export const throws = (e: IrExpr): boolean => e.kind === "libCall" && e.fn === "error.nodeThrow"; + +/** Receiver methods that read a collection or remove from it, and so add + * nothing to its graph. */ +const READ_RECEIVER = new Set([ + "get", + "has", + "delete", + "iterKey", + "iterValue", + "size", + "clear", + "length", + "at", + "includes", + "indexOf", + "lastIndexOf", + "indexEq", + "getNumber", + "join", + "nextPresent", + "slice", + "concatSpread", + "pop", + "shift", +]); + +/** The local a reference is read out of, through casts, checked reads and + * projections (`l.items[0]` → l), or the local itself. */ +function projectionBase(e: IrExpr, through?: (call: IrExpr) => IrExpr | null): string | null { + let at: IrExpr = e; + for (let i = 0; i < 64; i++) { + if (at.kind === "varRef") return at.localId; + if (at.kind === "call" && through !== undefined) { + const next = through(at); + if (next === null) return null; + at = next; + continue; + } + if (at.kind === "fieldGet" || at.kind === "recordGet" || at.kind === "recordKeyGet") + at = at.obj; + else if (at.kind === "arrayGet") at = at.arr; + else if (at.kind === "unionKeyGet") at = at.value; + else if ( + (at.kind === "mapIntrinsic" || at.kind === "setIntrinsic") && + (at.method === "get" || at.method === "iterKey" || at.method === "iterValue") + ) + at = at.receiver; + else if ( + at.kind === "unionNarrow" || + at.kind === "upcast" || + at.kind === "downcast" || + at.kind === "unionWrap" || + at.kind === "unionRetype" + ) + at = at.value; + else if (at.kind === "ternary" && throws(at.then)) at = at.else_; + else if (at.kind === "ternary" && throws(at.else_)) at = at.then; + else return null; + } + return null; +} + +/** The local whose graph a store into `target` writes (`l.f.items` → l). */ +export function storeTarget(target: IrExpr | undefined): string | null { + let at: IrExpr | undefined = target; + for (let i = 0; at !== undefined && i < 64; i++) { + if (at.kind === "varRef") return at.localId; + if (at.kind === "fieldGet" || at.kind === "recordGet" || at.kind === "recordKeyGet") + at = at.obj; + else if (at.kind === "arrayGet") at = at.arr; + else if (at.kind === "unionNarrow" || at.kind === "upcast" || at.kind === "downcast") + at = at.value; + // A checked read (`entry.skus` on `entry | undefined`) throws on one side. + else if (at.kind === "ternary" && throws(at.then)) at = at.else_; + else if (at.kind === "ternary" && throws(at.else_)) at = at.then; + else return null; + } + return null; +} + +/** An expression that produces a value it allocates (a group member). */ +export function source(e: Node): boolean { + if (!isExpr(e)) return false; + if (ALLOCATIONS.has(e.kind)) return true; + if (e.kind === "arrIntrinsic") return FRESH_ARRAY_METHODS.has(e.method); + if (e.kind === "strIntrinsic") return e.method === "split"; + if (e.kind === "libCall") return e.fn === "json.parse"; + if (e.kind === "dynCheck") return e.value.kind === "libCall" && e.value.fn === "json.parse"; + if (e.kind === "call" || e.kind === "callValue") return refType(e.type); + return false; +} + +/** Store-like uses: the value goes into the graph of `target`'s value. */ +export function storeOf(value: Node, parent: Node): IrExpr | undefined { + switch (parent.kind) { + case "fieldSet": + case "recordSet": + case "recordKeySet": + return same(parent.value, value) ? parent.obj : undefined; + case "arraySet": + return same(parent.value, value) ? parent.arr : undefined; + case "arrIntrinsic": + return (parent.method === "push" || parent.method === "unshift") && + indexOfNode(parent.args, value) >= 0 + ? parent.receiver + : undefined; + case "mapIntrinsic": + return parent.method === "set" && indexOfNode(parent.args, value) >= 0 + ? parent.receiver + : undefined; + case "setIntrinsic": + return parent.method === "add" && indexOfNode(parent.args, value) >= 0 + ? parent.receiver + : undefined; + default: + return undefined; + } +} + +/** Content positions: the value becomes part of the new value `parent`. */ +export function contentOf(value: Node, parent: Node): boolean { + switch (parent.kind) { + case "arrayLit": + case "recordLit": + case "new": + case "mapNew": + case "setNew": + return !same(parent, value); + case "recordClone": + return !same(parent.source, value); + default: + return false; + } +} + +/** Whether `parent` is a fresh array holding the elements of `child`, an + * array of references (`xs.slice()`, `a.concat(b)`): the elements share the + * fate of the copy. Strings are leaves, not members. */ +function copiesElements(child: Node, parent: Node | undefined): boolean { + if (parent === undefined || parent.kind !== "arrIntrinsic" || !isExpr(child)) return false; + if (!FRESH_ARRAY_METHODS.has(parent.method) && !SPREAD_METHODS.has(parent.method)) return false; + if ( + !same(parent.receiver, child) && + !(parent.method === "concatSpread" && indexOfNode(parent.args, child) >= 0) + ) + return false; + const t = child.type; + return t.kind === "array" && refType(t.elem); +} + +/** Array intrinsics that copy their array arguments' elements. */ +const SPREAD_METHODS = new Set(["pushSpread", "unshiftSpread", "concatSpread"]); + +/** Array intrinsics that call their function-typed arguments natively. */ +const CALLBACK_INTRINSICS = new Set(["sortDense", "sortValues"]); +const callbackArg = (child: Node, parent: Node | undefined): boolean => + parent !== undefined && + parent.kind === "arrIntrinsic" && + CALLBACK_INTRINSICS.has(parent.method) && + indexOfNode(parent.args, child) >= 0 && + isExpr(child) && + child.type.kind === "func"; + +type Use = "bound" | "kept" | "returned" | "stored" | "content" | "internal" | "thrown" | "escapes"; +interface UseInfo { + bound?: string; + /** For "internal": the index of the call in `parents`. */ + at?: number; + target?: string | null; +} + +export function analyzeRegionGroups(mod: IrModule): RegionGroups { + const fns = new Map(); + for (const fn of mod.functions) fns.set(fn.name, fn); + const vt = new VirtualTargets(mod.classes ?? []); + const suspends = (fn: IrFunction): boolean => fn.async === true || fn.generator !== undefined; + // Who refers to each function: by calls (direct, virtual, constructions), + // and by closures too. A function's summaries depend only on those of + // the functions it refers to, so the fixpoints below revisit just the + // referrers of a function whose summary changed. + const callers = new Map>(); + const referrers = new Map>(); + const directAlloc = new Set(); + for (const fn of mod.functions) { + const note = (map: Map>, name: string): void => { + let set = map.get(name); + if (set === undefined) map.set(name, (set = new Set())); + set.add(fn.name); + }; + forEachNode(fn.body, (node) => { + if (node.kind === "call") { + note(callers, node.callee); + note(referrers, node.callee); + } else if (node.kind === "virtualCall") + for (const t of virtualTargetsOf(vt, node) ?? []) { + note(callers, t); + note(referrers, t); + } + else if (node.kind === "new") { + const ctor = `%${node.className}.constructor`; + note(callers, ctor); + note(referrers, ctor); + } else if (node.kind === "closure") note(referrers, node.fnName); + if (source(node) && node.kind !== "call") directAlloc.add(fn.name); + }); + } + /** Runs `step` on the functions named, and again on the referrers of each + * whose summary it reports changed, until nothing changes. */ + const solve = ( + names: Iterable, + by: Map>, + step: (fn: IrFunction) => boolean, + ): void => { + const queue = [...names]; + const queued = new Set(queue); + while (queue.length > 0) { + const name = queue.pop()!; + queued.delete(name); + const fn = fns.get(name); + if (fn === undefined || !step(fn)) continue; + for (const r of by.get(name) ?? new Set()) + if (!queued.has(r)) { + queued.add(r); + queue.push(r); + } + } + }; + + // Deep summaries. + const deep = new Map>(); + // Internal parameters: ones a leak-free function only borrows, returns, or + // stores into the graphs of values it allocates (so an argument a caller + // allocated stays inside the caller's group). Optimistic, refined below. + const internal = new Map>(); + // Callback parameters (see below), and the function being analyzed. + const callbacks = new Map>(); + let current: IrFunction | null = null; + // While finding roots: a value passed to the function's own callback + // parameter leaves the frame (its callers' callbacks are unknown here). + let framePass = false; + const keeps = (child: Node, parent: Node | undefined): boolean => { + if (parent?.kind === "virtualCall") { + const index = indexOfNode(parent.args, child); + const targets = virtualTargetsOf(vt, parent); + return ( + index >= 0 && + targets !== null && + targets.length > 0 && + targets.every((t) => deep.get(t)?.has(index) === true) + ); + } + // A spread copies the array's elements into another array: they + // escape with it. + if ( + parent?.kind === "arrIntrinsic" && + SPREAD_METHODS.has(parent.method) && + !same(parent.receiver, child) && + isExpr(child) + ) { + const t = child.type; + return t.kind !== "array" || !refType(t.elem); + } + if (callbackArg(child, parent)) return true; + if (parent?.kind === "jsonStringify") return true; + // Comparisons only read their operands. + if (parent?.kind === "strCmp" || parent?.kind === "strEq") return true; + if (parent?.kind === "bin" && !refType(parent.type)) return true; + return keepsInFrame(child, parent, deep); + }; + + /** How the value of `node` (`parents` its ancestors) is used, after the + * expressions that pass it on (and, with `projections`, the reads of a + * reference out of it). */ + const useOf = (node: Node, parents: readonly Node[], projections: boolean, out: UseInfo): Use => { + let value: Node = node; + let at = parents.length - 1; + while ( + at >= 0 && + (transparent(value, parents[at]) || + copiesElements(value, parents[at]) || + (projections && projection(value, parents[at]))) + ) + value = parents[at--]!; + const p = at >= 0 ? parents[at] : undefined; + if (p === undefined) return "escapes"; + if ( + (p.kind === "varDecl" && p.init !== null && same(p.init, value)) || + (p.kind === "assign" && same(p.value, value)) + ) { + out.bound = p.localId; + return "bound"; + } + if (p.kind === "return") return "returned"; + if (p.kind === "throw") return "thrown"; + if (p.kind === "exprStmt") return "kept"; + const target = storeOf(value, p); + if (target !== undefined) { + out.target = storeTarget(target); + return "stored"; + } + if (contentOf(value, p)) return "content"; + if (keeps(value, p)) return "kept"; + if (p.kind === "callValue" && current !== null && !framePass) { + const i = indexOfNode(p.args, value) >= 0 ? paramIndex(current, p.callee) : -1; + if (i >= 0 && callbacks.get(current.name)!.has(i)) return "kept"; + } + if (p.kind === "call") { + const c = p; + const i = indexOfNode(c.args, value); + // The callee may hand the value to the callbacks it is passed, which + // must keep it too. + if ( + i >= 0 && + internal.get(c.callee)?.has(i) === true && + [...(callbacks.get(c.callee) ?? new Set())].every((j) => callbackClean(c.args[j])) + ) { + out.at = at; + return "internal"; + } + } + return "escapes"; + }; + + /** How the result of the call at `parents[at]` is used, through calls + * that keep it in their own results (`report(select(load()))`). */ + const resultUse = (at: number, parents: readonly Node[], out: UseInfo): Use => { + let index = at; + for (let i = 0; i < 64; i++) { + const info: UseInfo = {}; + const use = useOf(parents[index]!, parents.slice(0, index), false, info); + if (use !== "internal") { + // Field by field: an Object.assign onto a record makes the + // self-hosted compiler read class and record properties dynamically. + if (info.bound !== undefined) out.bound = info.bound; + if (info.at !== undefined) out.at = info.at; + if (info.target !== undefined) out.target = info.target; + return use; + } + index = info.at!; + } + return "escapes"; + }; + /** `fn`'s body in preorder, with each node's parent (-1 for statements + * of the body), the uses of locals that can hold references, and the + * sources: walked once, for escapingLocals' many passes. */ + interface Flat { + nodes: Node[]; + parent: number[]; + refs: number[]; + sources: number[]; + } + const flats = new Map(); + const flatOf = (fn: IrFunction): Flat => { + let flat = flats.get(fn.name); + if (flat !== undefined) return flat; + flat = { nodes: [], parent: [], refs: [], sources: [] }; + const f = flat; + // Parents come before their children, so each node's parent is indexed. + const index = new Map(); + forEachNode(fn.body, (node, parent) => { + const i = f.nodes.length; + index.set(node, i); + f.nodes.push(node); + f.parent.push(parent === undefined ? -1 : index.get(parent)!); + if (source(node)) f.sources.push(i); + if (node.kind === "varRef" && (refType(node.type) || node.type.kind === "string")) + f.refs.push(i); + }); + flats.set(fn.name, flat); + return flat; + }; + /** The ancestors of node `i` of `flat`, outermost first, into `out`. */ + const ancestors = (flat: Flat, i: number, out: Node[]): Node[] => { + out.length = 0; + for (let p = flat.parent[i]!; p >= 0; p = flat.parent[p]!) out.push(flat.nodes[p]!); + out.reverse(); + return out; + }; + /** Locals of `fn` whose value (or a reference read out of it) escapes. + * With `members` (the locals bound to group members), a value may also be + * returned, be content of a new value, or be stored into a member's + * graph. */ + const escapingLocals = ( + fn: IrFunction, + members: Set | null, + classes?: Map, + ): Set => { + const out = new Set(); + const add = (id: string): void => { + out.add(id); + }; + const own = new Set(fn.locals.map((l) => l.id)); + const group = new Map(); + const find = (id: string): string => { + let r = id; + while (group.has(r) && group.get(r) !== r) r = group.get(r)!; + return r; + }; + const unite = (a: string, b: string): void => { + const ra = find(a), + rb = find(b); + if (ra !== rb) group.set(ra, rb); + }; + // Copies: locals bound to a local or to a fresh copy of one (`slice`). + // A store of a value read out of a copy into the graph of another copy + // adds no reference into the group, unless a member went into a copy. + const copy = new Map(); + const findCopy = (id: string): string => { + let r = id; + while (copy.has(r) && copy.get(r) !== r) r = copy.get(r)!; + return r; + }; + const uniteCopy = (a: string, b: string): void => { + const ra = findCopy(a), + rb = findCopy(b); + if (ra !== rb) copy.set(ra, rb); + }; + const filled = new Set(); + const selfStores: [string, string][] = []; + const flat = flatOf(fn); + const parents: Node[] = []; + if (members !== null) + for (const i of flat.sources) { + const node = flat.nodes[i]!; + ancestors(flat, i, parents); + // A copy class holds something new once a value it did not have + // goes into it: stored into its graph, or bound to one of its locals + // (`visited = new Map(); this.visited = visited`). A `slice` of a + // local in the class holds only what the class held. + const info: UseInfo = {}; + const use = useOf(node, parents, false, info); + if (use === "stored" && info.target) filled.add(info.target); + if (use === "bound" && !(node.kind === "arrIntrinsic" && node.method === "slice")) + filled.add(info.bound!); + } + // Uses of locals that can hold references (a value without references + // cannot keep a member). + for (const i of flat.refs) { + const node = flat.nodes[i]!; + if (node.kind !== "varRef") continue; + ancestors(flat, i, parents); + const id = node.localId; + const info: UseInfo = {}; + const use = useOf(node, parents, true, info); + if (use === "bound") { + if (own.has(info.bound!)) { + unite(id, info.bound!); + uniteCopy(id, info.bound!); + } else add(id); + } else if (use === "internal" && members === null) { + // The callee keeps the value only in its result: the value stays + // in this frame while the result does (a module global needs no + // frame to hold it). + const result: UseInfo = {}; + const r = resultUse(info.at!, parents, result); + if (r === "bound" && own.has(result.bound!)) { + if (own.has(id)) unite(id, result.bound!); + } else if (r !== "kept") add(id); + } else if (use === "thrown") { + // A thrown value leaves the frame and outlives the group. + add(id); + } else if (use === "returned" || use === "content" || use === "internal") { + if (members === null) add(id); + } else if (use === "stored") { + if (members?.has(id) && info.target) filled.add(info.target); + if ( + members !== null && + info.target !== null && + info.target !== undefined && + !members.has(info.target) && + own.has(info.target) + ) + selfStores.push([id, info.target]); + else if ( + members === null || + info.target === null || + info.target === undefined || + !members.has(info.target) + ) + add(id); + } else if (use === "escapes") add(id); + // `const copy = id.slice()`. + // An optional chain on a Node, or a field read in the same condition + // as its kind test, tests every Node kind in turn: the kind is read + // after an undefined check, and the fields in a statement of their own. + const p = parents[parents.length - 1]; + if (members !== null && p !== undefined && p.kind === "arrIntrinsic") { + if (same(p.receiver, node) && p.method === "slice") { + const copied: UseInfo = {}; + if (useOf(p, parents.slice(0, -1), false, copied) === "bound" && own.has(copied.bound!)) + uniteCopy(id, copied.bound!); + } + } + } + const filledCopies = new Set([...filled].map(findCopy)); + for (const [id, target] of selfStores) + if (findCopy(id) !== findCopy(target) || filledCopies.has(findCopy(id))) add(id); + for (const local of fn.locals) if (local.boxed === true) out.add(local.id); + const roots = new Set(); + for (const id of out) roots.add(find(id)); + for (const local of fn.locals) if (roots.has(find(local.id))) out.add(local.id); + if (classes !== undefined) for (const local of fn.locals) classes.set(local.id, find(local.id)); + return out; + }; + + const solveDeep = (): void => { + for (const fn of mod.functions) { + const safe = new Set(); + if (!suspends(fn)) + fn.params.forEach((param, i) => { + const local = fn.locals.find((l) => l.id === param.localId); + if ((refType(param.type) || param.type.kind === "string") && local?.boxed !== true) + safe.add(i); + }); + deep.set(fn.name, safe); + } + solve(fns.keys(), referrers, (fn) => { + const safe = deep.get(fn.name)!; + if (safe.size === 0) return false; + const esc = escapingLocals(fn, null); + let changed = false; + fn.params.forEach((param, i) => { + if (safe.has(i) && esc.has(param.localId)) { + safe.delete(i); + changed = true; + } + }); + return changed; + }); + }; + solveDeep(); + + // Leak-free and fresh functions, optimistic. + const leakFree = new Set(); + const fresh = new Set(); + for (const fn of mod.functions) { + if (suspends(fn)) continue; + leakFree.add(fn.name); + if (refType(fn.returnType)) fresh.add(fn.name); + const inner = new Set(); + fn.params.forEach((param, i) => { + const local = fn.locals.find((l) => l.id === param.localId); + if ((refType(param.type) || param.type.kind === "string") && local?.boxed !== true) + inner.add(i); + }); + internal.set(fn.name, inner); + } + let internalChanged = false; + const unwrap = (e: Node): Node => { + let at: Node = e; + for (let i = 0; i < 16; i++) { + if ( + at.kind === "unionWrap" || + at.kind === "upcast" || + at.kind === "downcast" || + at.kind === "unionRetype" + ) + at = at.value; + else break; + } + return at; + }; + // Callback parameters: parameters a function calls (or passes on to a + // callback parameter). Each argument for one must be a leak-free closure, + // or a callback parameter of the caller. + const reassignedMemo = new Map>(); + const reassignedIn = (fn: IrFunction): Set => { + let out = reassignedMemo.get(fn.name); + if (out !== undefined) return out; + out = new Set(); + for (const node of flatOf(fn).nodes) if (node.kind === "assign") out.add(node.localId); + for (const l of fn.locals) if (l.boxed === true) out.add(l.id); + reassignedMemo.set(fn.name, out); + return out; + }; + const paramIndex = (fn: IrFunction, e: Node): number => { + const u = unwrap(e); + if (u.kind !== "varRef") return -1; + const id = u.localId; + if (reassignedIn(fn).has(id)) return -1; + return fn.params.findIndex((p) => p.localId === id); + }; + for (const fn of mod.functions) callbacks.set(fn.name, new Set()); + solve(fns.keys(), callers, (fn) => { + const set = callbacks.get(fn.name)!; + const before = set.size; + for (const node of flatOf(fn).nodes) { + if (node.kind === "callValue") { + const i = paramIndex(fn, node.callee); + if (i >= 0) set.add(i); + } else if (node.kind === "arrIntrinsic" && CALLBACK_INTRINSICS.has(node.method)) { + for (const a of node.args) + if (a.type.kind === "func") { + const i = paramIndex(fn, a); + if (i >= 0) set.add(i); + } + } else if (node.kind === "call") { + for (const j of callbacks.get(node.callee) ?? new Set()) { + const arg = node.args[j]; + const i = arg === undefined ? -1 : paramIndex(fn, arg); + if (i >= 0) set.add(i); + } + } + } + return set.size !== before; + }); + const callbackClean = (arg: IrExpr | undefined): boolean => { + if (arg === undefined) return true; + const u = unwrap(arg); + if (u.kind === "closure") { + // The callee passes the callback values it borrows (or that came from + // its caller): its parameters must stay internal. + const name = u.fnName; + const target = fns.get(name); + if (target === undefined || !leakFree.has(name)) return false; + const inner = internal.get(name)!; + return target.params.every((p, i) => !refType(p.type) || inner.has(i)); + } + if (current === null || framePass) return false; + const i = paramIndex(current, u); + return i >= 0 && callbacks.get(current.name)!.has(i); + }; + const calleesClean = (node: Node): boolean => { + if (node.kind === "call") { + if (!leakFree.has(node.callee)) return false; + for (const j of callbacks.get(node.callee) ?? new Set()) + if (!callbackClean(node.args[j])) return false; + return true; + } + if (node.kind === "callValue" && current !== null) { + const i = paramIndex(current, node.callee); + return i >= 0 && callbacks.get(current.name)!.has(i); + } + if (node.kind === "arrIntrinsic" && CALLBACK_INTRINSICS.has(node.method)) + return node.args.every((a) => a.type.kind !== "func" || callbackClean(a)); + if (node.kind === "virtualCall") { + const t = virtualTargetsOf(vt, node); + return t !== null && t.length > 0 && t.every((x) => leakFree.has(x)); + } + if ( + node.kind === "callValue" || + node.kind === "newValue" || + node.kind === "dynCall" || + node.kind === "dynInvoke" + ) + return false; + if (node.kind === "new") return leakFree.has(`%${node.className}.constructor`); + return true; + }; + /** The values `e` can produce, through casts and conditionals. */ + const leaves = (e: Node, out: Node[] = []): Node[] => { + const u = unwrap(e); + if (u.kind === "ternary") { + // A side that throws produces no value. + if (!throws(u.then)) leaves(u.then, out); + if (!throws(u.else_)) leaves(u.else_, out); + } else out.push(u); + return out; + }; + /** A source whose value is a new group member (a call result only when + * the callee is fresh). */ + const memberSource = (u: Node): boolean => + source(u) && u.kind !== "callValue" && (u.kind !== "call" || fresh.has(u.callee)); + /** What analyze reads of `fn` that no summary changes: the values bound + * to each local, the stores into locals' graphs, and the locals that are + * not closed (see analyze). */ + const bodyFacts = new Map< + string, + { bindings: Map; stores: [string, Node][]; open: Set } + >(); + const factsOf = ( + fn: IrFunction, + ): { bindings: Map; stores: [string, Node][]; open: Set } => { + const memo = bodyFacts.get(fn.name); + if (memo !== undefined) return memo; + const bindings = new Map(); + const scan = (node: Node): void => { + if (node.kind === "varDecl" || node.kind === "assign") { + const v = node.kind === "varDecl" ? node.init : node.value; + if (v !== null) bindings.set(node.localId, [...(bindings.get(node.localId) ?? []), v]); + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) scan(k); + }; + for (const s of fn.body) scan(s); + const stores: [string, Node][] = []; + const open = new Set(); + { + const parents: Node[] = []; + const walk = (node: Node): void => { + const parent = parents[parents.length - 1]; + if (parent !== undefined) { + const t = storeOf(node, parent); + if (t !== undefined) { + const target = storeTarget(t); + if (target !== null) stores.push([target, node]); + } + } + if (node.kind === "varRef") { + const id = node.localId; + let value: Node = node; + let at = parents.length - 1; + while (at >= 0 && (transparent(value, parents[at]) || projection(value, parents[at]))) + value = parents[at--]!; + const p = parents[at]; + if (p !== undefined && isExpr(p)) { + const x = p; + if ( + x.kind === "call" || + x.kind === "virtualCall" || + x.kind === "callValue" || + x.kind === "new" || + x.kind === "newValue" || + x.kind === "dynCall" || + x.kind === "dynInvoke" || + x.kind === "libCall" + ) + open.add(id); + else if ( + (x.kind === "arrIntrinsic" || + x.kind === "mapIntrinsic" || + x.kind === "setIntrinsic") && + same(x.receiver, value) && + !READ_RECEIVER.has(x.method) && + !x.method.startsWith("iter") && + storeOf(x.args[0] ?? x, x) === undefined + ) + open.add(id); + } + } + const kids: Node[] = []; + children(node, kids); + parents.push(node); + for (const kid of kids) walk(kid); + parents.pop(); + }; + for (const st of fn.body) walk(st); + } + const facts = { bindings, stores, open }; + bodyFacts.set(fn.name, facts); + return facts; + }; + const analyze = (fn: IrFunction): { leakFree: boolean; fresh: boolean } => { + current = fn; + // Member locals: bound only to sources, other members, or non-references. + const members = new Set(fn.locals.filter((l) => l.boxed !== true).map((l) => l.id)); + for (const p of fn.params) members.delete(p.localId); + // A constructor's `this` is the object its `new` allocated. + const ctor = fn.name.endsWith(".constructor") && fn.params.length > 0; + const thisId = ctor ? fn.params[0]!.localId : null; + // Closed members: members whose graph holds only members and leaves + // (values without a graph: numbers, strings). A reference read out of a + // closed member is a member too. A local is not closed when it is passed + // to a call (which may store into its graph) or used as the receiver of + // a method that adds what this analysis does not see. + const { bindings, stores, open } = factsOf(fn); + const closed = new Set([...members].filter((id) => !open.has(id) && id !== thisId)); + const leaf = (u: Node): boolean => isExpr(u) && !refType(u.type); + const closedSource = (u: Node): boolean => { + if (u.kind === "libCall") return u.fn === "json.parse"; + if (u.kind === "dynCheck") return closedSource(u.value); + if (u.kind === "strIntrinsic") return u.method === "split"; + if ( + u.kind !== "arrayLit" && + u.kind !== "recordLit" && + u.kind !== "mapNew" && + u.kind !== "setNew" + ) + return false; + const kids: Node[] = []; + children(u, kids); + return kids.every((k) => !isExpr(k) || closedValue(k)); + }; + const closedValue = (v: Node): boolean => + leaves(v).every((u) => { + if (leaf(u) || closedSource(u)) return true; + const base = isExpr(u) ? projectionBase(u) : null; + return base !== null && closed.has(base); + }); + for (let again = true; again;) { + again = false; + for (const id of [...members]) { + const list = bindings.get(id) ?? []; + if (id === thisId) continue; + const ok = + list.length > 0 && + list.every((v) => + leaves(v).every((u) => { + if (memberSource(u) || leaf(u)) return true; + if (u.kind === "varRef") return members.has(u.localId); + const base = isExpr(u) ? projectionBase(u) : null; + return base !== null && closed.has(base); + }), + ); + if (!ok) { + members.delete(id); + closed.delete(id); + again = true; + } + } + for (const id of [...closed]) { + const ok = + members.has(id) && + (bindings.get(id) ?? []).every(closedValue) && + stores.every(([target, value]) => target !== id || closedValue(value)); + if (!ok) { + closed.delete(id); + again = true; + } + } + // A local bound to a reference read out of another shares its graph. + for (const [id, list] of bindings) + if (!closed.has(id)) + for (const v of list) + for (const u of leaves(v)) { + const base = isExpr(u) ? projectionBase(u) : null; + if (base !== null && base !== id && closed.delete(base)) again = true; + } + } + if (thisId !== null) members.add(thisId); + // Members that escape (other than by return or into a member's graph) + // are no longer members; repeat, since a store into a former member's + // graph now escapes too. + let esc = escapingLocals(fn, members); + for (let again = true; again;) { + again = false; + for (const id of esc) if (members.delete(id)) again = true; + if (again) esc = escapingLocals(fn, members); + } + // Every source must die, be returned, or go into a member's graph; a + // constructor's `this` must stay a member. + const inner = internal.get(fn.name)!; + fn.params.forEach((param, i) => { + if (inner.has(i) && esc.has(param.localId)) { + inner.delete(i); + internalChanged = true; + } + }); + let clean = thisId === null || members.has(thisId); + let returnsFresh = true; + const parents: Node[] = []; + const walk = (node: Node): void => { + if (!calleesClean(node)) clean = false; + if (source(node)) { + const info: UseInfo = {}; + const use = useOf(node, parents, false, info); + if (use === "bound") { + if (esc.has(info.bound!) || !fn.locals.some((l) => l.id === info.bound)) { + clean = false; + } + } else if (use === "stored") { + if (info.target === null || info.target === undefined || !members.has(info.target)) { + clean = false; + } + } else if (use === "escapes") { + clean = false; + } else if (use === "thrown") { + // A thrown value outlives the group, so it may hold no member and + // nothing allocated with it (a message string is fine). + const kids: Node[] = []; + children(node, kids); + for (const k of kids) + if ( + isExpr(k) && + refType(k.type) && + leaves(k).some((u) => { + if (source(u)) return true; + const base = isExpr(u) ? projectionBase(u) : null; + return base !== null && members.has(base); + }) + ) + clean = false; + } + } + if (node.kind === "return") { + const v = node.value; + if (v !== null && refType(v.type)) { + for (const u of leaves(v)) { + if (!(memberSource(u) || (u.kind === "varRef" && members.has(u.localId)))) + returnsFresh = false; + if (u.kind === "call" && !fresh.has(u.callee)) returnsFresh = false; + } + } + } + const kids: Node[] = []; + children(node, kids); + parents.push(node); + for (const kid of kids) walk(kid); + parents.pop(); + }; + for (const s of fn.body) walk(s); + current = null; + return { leakFree: clean, fresh: clean && returnsFresh }; + }; + solve(leakFree, referrers, (fn) => { + if (!leakFree.has(fn.name)) return false; + internalChanged = false; + const r = analyze(fn); + if (!r.leakFree) { + leakFree.delete(fn.name); + fresh.delete(fn.name); + internal.get(fn.name)!.clear(); + return true; + } + return (!r.fresh && fresh.delete(fn.name)) || internalChanged; + }); + + // With internal parameters known, a value passed to one stays in the + // frame while the call's result does: solve the deep summaries again. + solveDeep(); + // Functions that allocate, directly or through what they call. + const allocates = new Set(); + solve(directAlloc, callers, (fn) => { + if (allocates.has(fn.name)) return false; + allocates.add(fn.name); + return true; + }); + // Roots, and call regions: calls of leak-free functions that allocate and + // return no reference, so all they allocate is dead when they return. + const roots = new Set(); + const calls = new Set(); + /** Whether the operands of `node` (evaluated inside its group) are clean: + * every call in them is leak-free, no library call returns a reference + * (the runtime may keep it: `process.argv`'s cached array), and every value they + * allocate goes to `node` as an internal or borrowed argument, or into the + * value of another operand. */ + const operandsClean = (node: Node, parents: readonly Node[]): boolean => { + const stack: Node[] = [...parents, node]; + let ok = true; + const visit = (n: Node): void => { + if (!ok) return; + if (!same(n, node)) { + if (!calleesClean(n)) ok = false; + if (n.kind === "libCall" && n.fn !== "json.parse" && refType(n.type)) ok = false; + // `JSON.parse(text) as T`: the parse is the checked value itself. + const checked = node.kind === "dynCheck" && same(node.value, n); + if (source(n) && !checked) { + const info: UseInfo = {}; + const use = useOf(n, stack, false, info); + const at = info.at ?? -1; + if ( + !(use === "kept" || use === "content" || (use === "internal" && at >= parents.length)) + ) + ok = false; + } + } + const kids: Node[] = []; + children(n, kids); + const inner = !same(n, node); + if (inner) stack.push(n); + for (const k of kids) visit(k); + if (inner) stack.pop(); + }; + visit(node); + return ok; + }; + const drops = new Map(); + const rootLocals = new Map>(); + const rootFunction = new Map(); + framePass = true; + for (const fn of mod.functions) { + if (suspends(fn)) continue; + current = fn; + const classes = new Map(); + const esc = escapingLocals(fn, null, classes); + const own = new Set(fn.locals.map((l) => l.id)); + const paths = declarationPaths(fn); + /** The local of `bound`'s class whose scope holds every other one's, + * or undefined when no one does. */ + const classOf = (bound: string): string[] => { + const cls = classes.get(bound); + return fn.locals.filter((l) => classes.get(l.id) === cls).map((l) => l.id); + }; + const dropLocal = (bound: string): string | undefined => { + const members = classOf(bound); + const path = (id: string): readonly number[] => paths.get(id) ?? []; + return members.find((m) => + members.every((o) => { + const a = path(m); + const b = path(o); + return a.length <= b.length && a.every((x, i) => b[i] === x); + }), + ); + }; + const parents: Node[] = []; + const walk = (node: Node): void => { + const isRoot = + isExpr(node) && + ((node.kind === "call" && fresh.has(node.callee)) || + (node.kind === "new" && leakFree.has(`%${node.className}.constructor`)) || + ((node.kind === "libCall" || node.kind === "dynCheck" || node.kind === "strIntrinsic") && + source(node)) || + (node.kind === "arrIntrinsic" && source(node))); + const callbacksClean = + node.kind !== "call" || + [...(callbacks.get(node.callee) ?? new Set())].every((j) => + callbackClean(node.args[j]), + ); + const clean = + (isRoot || node.kind === "call") && callbacksClean && operandsClean(node, parents); + if ( + node.kind === "call" && + clean && + !refType(node.type) && + // A constructor (a `super(...)` call) stores into `this`, which its + // caller holds: it is leak-free only as part of a `new`. + !node.callee.endsWith(".constructor") && + leakFree.has(node.callee) && + allocates.has(node.callee) + ) { + calls.add(node); + rootFunction.set(node, fn); + } + if (isRoot && clean) { + const info: UseInfo = {}; + let use = useOf(node, parents, false, info); + if (use === "internal") use = resultUse(info.at!, parents, info); + rootFunction.set(node, fn); + if (use === "kept") { + roots.add(node); + drops.set(node, null); + rootLocals.set(node, new Set()); + } else if (use === "bound" && own.has(info.bound!) && !esc.has(info.bound!)) { + const local = dropLocal(info.bound!); + // An assignment in a loop would add a group per iteration to a + // list that is dropped only when the local's scope exits. + let binding = parents.length - 1; + while ( + binding >= 0 && + parents[binding]!.kind !== "varDecl" && + parents[binding]!.kind !== "assign" + ) + binding--; + const reassignedInLoop = + binding >= 0 && + parents[binding]!.kind === "assign" && + parents.slice(0, binding).some((p) => LOOP_KINDS.has(p.kind)); + if (local !== undefined && !reassignedInLoop) { + roots.add(node); + drops.set(node, local); + rootLocals.set(node, new Set(classOf(info.bound!))); + } + } + } + const kids: Node[] = []; + children(node, kids); + parents.push(node); + for (const kid of kids) walk(kid); + parents.pop(); + }; + for (const s of fn.body) walk(s); + } + current = null; + // Functions that run inside an arena: what roots and call regions call, + // and what those call, with the closures they pass. + const reachable = new Set(); + const queue: string[] = []; + const reach = (name: string): void => { + if (fns.has(name) && !reachable.has(name)) { + reachable.add(name); + queue.push(name); + } + }; + const callees = (node: Node): void => { + if (node.kind === "call") reach(node.callee); + else if (node.kind === "virtualCall") + for (const t of virtualTargetsOf(vt, node) ?? []) reach(t); + else if (node.kind === "new") reach(`%${node.className}.constructor`); + else if (node.kind === "closure") reach(node.fnName); + }; + const scanAll = (node: Node, visit: (n: Node) => void): void => { + visit(node); + const kids: Node[] = []; + children(node, kids); + for (const k of kids) scanAll(k, visit); + }; + for (const r of [...roots, ...calls]) scanAll(r, callees); + while (queue.length > 0) { + const fn = fns.get(queue.pop()!)!; + for (const st of fn.body) scanAll(st, callees); + } + const closed = new Set(); + return { + roots, + calls, + drops, + rootLocals, + rootFunction, + reachable, + closed, + fresh, + leakFree, + allocates, + }; +} + +/** For each local declared in `fn`, the statement lists that enclose its + * declaration, outermost first (the body included); a declaration inside an + * expression (a sequence's statements) is nested in the statement that + * holds it. Parameters and locals without a declaration have none: the + * function's own scope. A `for…of` variable counts as declared in the list + * holding the loop. */ +function declarationPaths(fn: IrFunction): Map { + const out = new Map(); + // Each statement list (and each expression holding one) gets an id. + let next = 0; + const walk = (list: readonly IrStmt[], path: readonly number[]): void => { + if (list.length === 0) return; + const here = [...path, next++]; + for (const stmt of list) { + if (stmt.kind === "varDecl" || stmt.kind === "forOf") out.set(stmt.localId, here); + for (const inner of stmtLists(stmt)) walk(inner, here); + // Statements inside the statement's expressions, one level deeper. + const visit = (e: IrExpr): boolean => { + if (e.kind === "seqExpr") walk(e.stmts, [...here, next++]); + everyExprChild(e, visit, () => true); + return true; + }; + everyStmtChild(stmt, visit, () => true); + } + }; + walk(fn.body, []); + return out; +} + +/** The statement lists directly inside `stmt`. */ +function stmtLists(stmt: IrStmt): readonly (readonly IrStmt[])[] { + switch (stmt.kind) { + case "if": + return stmt.else_ === null ? [stmt.then] : [stmt.then, stmt.else_]; + case "while": + case "doWhile": + case "for": + case "forOf": + case "block": + return [stmt.body]; + case "switch": + return stmt.cases.map((c) => c.body); + case "tryCatch": + return [stmt.tryBody, stmt.catchBody ?? [], stmt.finallyBody ?? []]; + default: + return []; + } +} + +/** + * The roots and call regions whose groups are closed: every reference an + * arena object of the group holds is to an arena object of the group or an + * immortal value, so dropping the arena releases nothing. + * + * `arenaType(t)` says whether a value of type `t` is a plain reference to + * an object the arena-aware constructors allocate (no box around it). + * + * A function is closed when every reference it stores (into a field, an + * element, a collection, or the content of a new object) is closed: + * - a value without references, an immortal literal, or null/undefined; + * - an object of an arena type it allocates, or the result of a call of a + * closed function whose results are closed; + * - a local bound only to closed values, or a reference of an arena type + * read out of one; + * - a constructor's `this`; + * - a parameter, or a reference read out of one: its callers must pass a + * closed value (the function's stored parameters). + * Every function it calls must be closed and get closed values for their + * stored parameters. A root's group is then closed when its callee is, + * its arguments for stored parameters are closed in its caller, and the + * caller stores only closed values into the root's graph afterwards (there, + * only values without references, literals, and references read out of the + * root's own locals are closed: the caller's own allocations are on the + * heap). Solved optimistically to a fixpoint. + */ +export function analyzeClosedGroups( + mod: IrModule, + groups: RegionGroups, + arenaType: (t: IrType) => boolean, + /** Whether an allocation or call site's objects take the arena. */ + kept: (e: Node) => boolean = () => true, +): Set { + const fns = new Map(); + for (const fn of mod.functions) fns.set(fn.name, fn); + const vt = new VirtualTargets(mod.classes ?? []); + const closedFn = new Set([...groups.reachable].filter((f) => groups.leakFree.has(f))); + const closedFresh = new Set([...closedFn].filter((f) => groups.fresh.has(f))); + const stored = new Map>(); + const storedOf = (name: string): Set => stored.get(name) ?? new Set(); + const leavesOf = (e: IrExpr, out: IrExpr[] = []): IrExpr[] => { + let at = e; + for (let i = 0; i < 16; i++) + if ( + at.kind === "unionWrap" || + at.kind === "upcast" || + at.kind === "downcast" || + at.kind === "unionRetype" + ) + at = at.value; + else break; + if (at.kind === "ternary") { + if (!throws(at.then)) leavesOf(at.then, out); + if (!throws(at.else_)) leavesOf(at.else_, out); + } else out.push(at); + return out; + }; + // Functions that return only references read out of one parameter (or + // null/undefined), such as the checked element read `%arr.idxOr`: a call + // of one reads out of that argument. + const projects = new Map(); + for (const fn of mod.functions) { + const assigned = new Set(); + let index: number | null = null; + let ok = true; + const visit = (node: Node): void => { + if (!ok) return; + if (node.kind === "assign") assigned.add(node.localId); + if (node.kind === "return") { + const v = node.value; + if (v === null) ok = false; + else + for (const u of leavesOf(v)) { + if (u.kind === "unitLit") continue; + const base = projectionBase(u); + const i = base === null ? -1 : fn.params.findIndex((p) => p.localId === base); + if (i < 0 || (index !== null && index !== i)) ok = false; + else index = i; + } + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) visit(k); + }; + if (!refType(fn.returnType)) continue; + for (const st of fn.body) visit(st); + if (ok && index !== null && !assigned.has(fn.params[index]!.localId)) + projects.set(fn.name, index); + } + const through = (call: IrExpr): IrExpr | null => { + if (call.kind !== "call") return null; + const i = projects.get(call.callee); + return i === undefined ? null : (call.args[i] ?? null); + }; + /** Whether `e` (in `fn`, `own` its closed locals, `inArena` whether its + * allocations are arena objects) is a closed value; parameters it reads + * go into `params`. */ + const closedValue = ( + e: Node, + fn: IrFunction, + own: ReadonlySet, + inArena: boolean, + params: Set, + ): boolean => { + if (!isExpr(e)) return false; + const t = e.type; + if (t.kind === "string") return e.kind === "strLit"; + // Any other counted value (a typed array, a promise) is a heap object. + if (!refType(t) && !isRefCounted(t)) return true; + if (!arenaType(t)) return false; + return leavesOf(e).every((u) => { + if (u.kind === "unitLit") return true; + if (u.kind === "recordLit" || u.kind === "new" || u.kind === "arrayLit") + return inArena && arenaType(u.type) && kept(u); + if (u.kind === "call" && !projects.has(u.callee)) + return inArena && closedFresh.has(u.callee) && arenaType(u.type) && kept(u); + const base = projectionBase(u, through); + if (base === null) return false; + const i = fn.params.findIndex((p) => p.localId === base); + if (i >= 0) { + if (i === 0 && fn.name.endsWith(".constructor") && u.kind === "varRef") return true; + params.add(i); + return arenaType(u.type); + } + return own.has(base) && arenaType(u.type); + }); + }; + /** Every reference store and content position in `fn`'s body, and every + * call with the arguments its callee stores. */ + const siteMemo = new Map(); + const sites = (fn: IrFunction): { value: Node; target: string | null }[] => { + const memo = siteMemo.get(fn.name); + if (memo !== undefined) return memo; + const out: { value: Node; target: string | null }[] = []; + siteMemo.set(fn.name, out); + const walk = (node: Node, parent: Node | undefined): void => { + if (parent !== undefined && isExpr(node)) { + const target = storeOf(node, parent); + if (target !== undefined) out.push({ value: node, target: storeTarget(target) }); + else if (contentOf(node, parent) && parent.kind !== "new") + out.push({ value: node, target: null }); + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) walk(k, node); + }; + for (const st of fn.body) walk(st, undefined); + return out; + }; + const bindingMemo = new Map>(); + const bindingsOf = (fn: IrFunction): Map => { + const memo = bindingMemo.get(fn.name); + if (memo !== undefined) return memo; + const out = new Map(); + bindingMemo.set(fn.name, out); + const scan = (node: Node): void => { + if (node.kind === "varDecl" || node.kind === "assign") { + const v = node.kind === "varDecl" ? node.init : node.value; + if (v !== null) out.set(node.localId, [...(out.get(node.localId) ?? []), v]); + } + // A loop variable holds elements read out of the iterable. + if (node.kind === "forOf") + out.set(node.localId, [...(out.get(node.localId) ?? []), node.iterable]); + const kids: Node[] = []; + children(node, kids); + for (const k of kids) scan(k); + }; + for (const st of fn.body) scan(st); + return out; + }; + // Written parameters: those whose graph a function may store into, itself + // or through what it calls (any function, not only arena-aware ones). + const writes = new Map>(); + /** The parameter whose graph `local` reaches into, through bindings to + * references read out of it, or -1. */ + const paramOf = (fn: IrFunction, bindings: Map, local: string): number => { + let at = local; + for (let i = 0; i < 16; i++) { + const p = fn.params.findIndex((q) => q.localId === at); + if (p >= 0) return p; + const list = bindings.get(at); + if (list === undefined || list.length !== 1) return -1; + const base = projectionBase(list[0]!); + if (base === null || base === at) return -1; + at = base; + } + return -1; + }; + // Each function's own writes, and the parameters it passes to calls: a + // parameter passed where the callee writes is written too. + const passed = new Map< + string, + { param: number; callees: readonly string[] | null; index: number }[] + >(); + const writers = new Map>(); + // Only what the functions holding roots call, transitively, is asked. + const asked = new Set(); + { + const queue = [ + ...new Set( + [...groups.roots, ...groups.calls].flatMap((r) => groups.rootFunction.get(r)?.name ?? []), + ), + ]; + for (const name of queue) asked.add(name); + while (queue.length > 0) { + const fn = fns.get(queue.pop()!); + if (fn === undefined) continue; + const visit = (n: Node): void => { + const targets = + n.kind === "call" + ? [n.callee] + : n.kind === "virtualCall" + ? (virtualTargetsOf(vt, n) ?? []) + : []; + for (const t of targets) + if (fns.has(t) && !asked.has(t)) { + asked.add(t); + queue.push(t); + } + const kids: Node[] = []; + children(n, kids); + for (const k of kids) visit(k); + }; + for (const st of fn.body) visit(st); + } + } + for (const fn of mod.functions) { + if (!asked.has(fn.name)) continue; + const bindings = bindingsOf(fn); + const set = new Set(); + for (const { target } of sites(fn)) + if (target !== null) { + const i = paramOf(fn, bindings, target); + if (i >= 0) set.add(i); + } + const list: { param: number; callees: readonly string[] | null; index: number }[] = []; + const visit = (n: Node): void => { + const args = argsOf(n); + if (args !== null) { + const callees = + n.kind === "call" + ? [n.callee] + : n.kind === "virtualCall" + ? (virtualTargetsOf(vt, n) ?? []) + : null; + args.forEach((a, j) => { + const base = projectionBase(a); + const i = base === null ? -1 : paramOf(fn, bindings, base); + if (i < 0) return; + if (callees === null || callees.length === 0 || callees.some((c) => !fns.has(c))) + set.add(i); + else { + list.push({ param: i, callees, index: j }); + for (const c of callees) { + let w = writers.get(c); + if (w === undefined) writers.set(c, (w = new Set())); + w.add(fn.name); + } + } + }); + } + const kids: Node[] = []; + children(n, kids); + for (const k of kids) visit(k); + }; + for (const st of fn.body) visit(st); + writes.set(fn.name, set); + passed.set(fn.name, list); + } + { + const queue = [...asked]; + const queued = new Set(queue); + while (queue.length > 0) { + const name = queue.pop()!; + queued.delete(name); + const set = writes.get(name)!; + const before = set.size; + for (const { param, callees, index } of passed.get(name)!) + if (callees!.some((c) => writes.get(c)?.has(index) === true)) set.add(param); + if (set.size !== before) + for (const r of writers.get(name) ?? new Set()) + if (!queued.has(r)) { + queued.add(r); + queue.push(r); + } + } + } + /** The locals of `fn` bound only to closed values. */ + const closedLocals = ( + fn: IrFunction, + inArena: boolean, + seed?: ReadonlySet, + ): Set => { + const bindings = bindingsOf(fn); + const own = new Set( + fn.locals + .filter((l) => l.boxed !== true && !fn.params.some((p) => p.localId === l.id)) + .map((l) => l.id), + ); + for (const id of seed ?? new Set()) own.add(id); + for (let again = true; again;) { + again = false; + for (const id of [...own]) { + if (seed?.has(id) === true) continue; + const list = bindings.get(id) ?? []; + if (!list.every((v) => closedValue(v, fn, own, inArena, new Set()))) { + own.delete(id); + again = true; + } + } + } + return own; + }; + /** Whether the calls in `node`'s subtree are of closed functions with + * closed arguments for their stored parameters. */ + const callsClosed = ( + node: Node, + fn: IrFunction, + own: ReadonlySet, + inArena: boolean, + params: Set, + ): boolean => { + let ok = true; + const visit = (n: Node): void => { + if (!ok) return; + if (n.kind === "call" || n.kind === "new" || n.kind === "virtualCall") { + const targets = + n.kind === "call" + ? [n.callee] + : n.kind === "new" + ? [`%${n.className}.constructor`] + : (virtualTargetsOf(vt, n) ?? []); + if (targets.length === 0) ok = false; + const shift = n.kind === "new" ? 1 : 0; + for (const t of targets) { + if (!closedFn.has(t)) ok = false; + for (const j of storedOf(t)) { + const arg = n.args[j - shift]; + if (arg !== undefined && !closedValue(arg, fn, own, inArena, params)) ok = false; + } + } + } else if (n.kind === "callValue" || n.kind === "dynCall" || n.kind === "dynInvoke") + ok = false; + else if ( + (n.kind === "arrIntrinsic" || n.kind === "mapIntrinsic" || n.kind === "setIntrinsic") && + !READ_RECEIVER.has(n.method) && + !ADDING_METHODS.has(n.method) && + n.args.some((a) => refType(a.type) || isRefCounted(a.type)) + ) + ok = false; + const kids: Node[] = []; + children(n, kids); + for (const k of kids) visit(k); + }; + visit(node); + return ok; + }; + for (let changed = true; changed;) { + changed = false; + for (const name of [...closedFn]) { + const fn = fns.get(name)!; + const params = new Set(); + const own = closedLocals(fn, true); + let ok = sites(fn).every(({ value }) => closedValue(value, fn, own, true, params)); + for (const st of fn.body) if (ok) ok = callsClosed(st, fn, own, true, params); + let returnsClosed = true; + const scanReturns = (node: Node): void => { + if (node.kind === "return") { + const v = node.value; + if (v !== null && !closedValue(v, fn, own, true, params)) returnsClosed = false; + } + const kids: Node[] = []; + children(node, kids); + for (const k of kids) scanReturns(k); + }; + for (const st of fn.body) scanReturns(st); + const before = storedOf(name); + if ([...params].some((i) => !before.has(i))) { + stored.set(name, new Set([...before, ...params])); + changed = true; + } + if (!ok) { + closedFn.delete(name); + closedFresh.delete(name); + changed = true; + } else if (!returnsClosed && closedFresh.delete(name)) changed = true; + } + } + const out = new Set(); + for (const root of [...groups.roots, ...groups.calls]) { + const fn = groups.rootFunction.get(root); + if (fn === undefined) continue; + const locals = groups.rootLocals.get(root) ?? new Set(); + const bindings = bindingsOf(fn); + /** Whether `local` reaches into the root's graph: one of its locals, + * or bound (a loop variable included) to a reference read out of one. */ + const intoRoot = (local: string, depth = 0): boolean => + locals.has(local) || + (depth < 16 && + (bindings.get(local) ?? []).some((v) => { + const base = projectionBase(v); + return base !== null && base !== local && intoRoot(base, depth + 1); + })); + // In the caller only the root's own locals and what is read out of them + // are closed references. + const params = new Set(); + let ok = callsClosed(root, fn, locals, false, params) && params.size === 0; + // After the arena closes, a call that is passed the root's value (or a + // reference read out of it) must not write into it. + const passes = (n: Node, parent: Node | undefined): void => { + if (!ok) return; + if (parent !== undefined && isExpr(n)) { + const args = argsOf(parent); + const j = args === null ? -1 : indexOfNode(args, n); + const base = projectionBase(n); + if (j >= 0 && refType(n.type) && (same(n, root) || (base !== null && intoRoot(base)))) { + const callees = + parent.kind === "call" + ? [parent.callee] + : parent.kind === "virtualCall" + ? (virtualTargetsOf(vt, parent) ?? []) + : []; + if (callees.length === 0 || callees.some((c) => writes.get(c)?.has(j) !== false)) + ok = false; + } + } + const kids: Node[] = []; + children(n, kids); + for (const k of kids) passes(k, n); + }; + if (ok && !groups.calls.has(root)) for (const st of fn.body) passes(st, undefined); + if (ok && locals.size > 0) + ok = sites(fn).every( + ({ value, target }) => + target === null || + !intoRoot(target) || + (closedValue(value, fn, locals, false, params) && params.size === 0), + ); + if (ok) out.add(root); + } + return out; +} diff --git a/packages/compiler/src/backend/llvm/region-ir.ts b/packages/compiler/src/backend/llvm/region-ir.ts new file mode 100644 index 000000000..405558f06 --- /dev/null +++ b/packages/compiler/src/backend/llvm/region-ir.ts @@ -0,0 +1,271 @@ +/* IR helpers shared by the region analyses (region-groups.ts): node + * traversal, allocation kinds, and how a use of a value passes it on or + * keeps it in the frame. */ +import type { IrExpr, IrStmt, IrType } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; + +export type Node = IrExpr | IrStmt; + +/** The kinds of heap allocation sites counted. */ +export const ALLOCATIONS = new Set([ + "arrayLit", + "arrayNewLen", + "mapNew", + "setNew", + "new", + "recordLit", + "recordClone", + "closure", +]); + +/** Array methods that return a fresh array without keeping the receiver. */ +export const FRESH_ARRAY_METHODS = new Set([ + "slice", + "concatSpread", + "toReversed", + "toSpliced", + "with", + "withUndefined", + "flatCopy", + "flatOne", + "toSortedPrimitive", + "splice", +]); +/** Array methods that return the receiver itself. */ +export const RECEIVER_RESULT = new Set([ + "reverse", + "fill", + "fillUndefined", + "copyWithin", + "sortPrimitive", +]); +/** Array methods whose arguments are only read (compared, copied from). */ +export const READ_ARGS = new Set([ + "includes", + "indexOf", + "indexEq", + "getNumber", + "join", + "slice", + "concatSpread", + "pushSpread", + "unshiftSpread", + "nextPresent", + "length", +]); + +export function refType(t: IrType): boolean { + return ( + t.kind === "array" || + t.kind === "map" || + t.kind === "set" || + t.kind === "object" || + t.kind === "record" || + t.kind === "func" || + t.kind === "union" + ); +} + +export function children(node: Node, out: Node[]): void { + const push = (child: Node): boolean => { + out.push(child); + return true; + }; + if ("type" in node && (node as IrExpr).type !== undefined && isExpr(node)) + everyExprChild(node as IrExpr, push, push); + else everyStmtChild(node as IrStmt, push, push); +} + +const STMT_KINDS = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "while", + "doWhile", + "switch", + "for", + "arraySet", + "arraySetLength", + "arraySetUndefined", + "arrayDelete", + "bytesSet", + "forOf", + "return", + "fieldSet", + "recordSet", + "recordKeySet", + "recordKeyDelete", + "break", + "continue", + "block", + "throw", + "runtimeFence", + "rethrow", + "tryCatch", +]); +export function isExpr(node: Node): node is IrExpr { + return !STMT_KINDS.has(node.kind); +} + +/** Whether `a` and `b` are the same node. */ +export function same(a: Node | undefined, b: Node): boolean { + return a !== undefined && a === b; +} + +/** Whether `parent` passes `child`'s value on as its own value (a branch of + * a conditional, a union wrap or narrow, a class up- or downcast, the + * result of a sequence), so the value's fate is the parent's. */ +export function transparent(child: Node, parent: Node | undefined): boolean { + if (parent === undefined) return false; + switch (parent.kind) { + case "ternary": + return same(parent.then, child) || same(parent.else_, child); + case "nullish": + case "logical": + return true; + case "seqExpr": + return same(parent.result, child); + case "unionWrap": + case "unionRetype": + case "unionNarrow": + case "upcast": + case "downcast": + return same(parent.value, child); + default: + return false; + } +} + +/** Whether a use of a local's value as `child` of `parent` keeps it in the + * frame (given the current parameter summaries). */ +export function keepsInFrame( + child: Node, + parent: Node | undefined, + summaries: ReadonlyMap>, +): boolean { + if (parent === undefined) return false; + switch (parent.kind) { + case "exprStmt": + return true; + case "arrayGet": + case "arrayHas": + case "arrayState": + case "arraySetUndefined": + case "arrayDelete": + case "arraySetLength": + case "arraySet": + return same(parent.arr, child); + case "fieldGet": + case "recordGet": + case "recordKeyGet": + case "recordHas": + case "recordOvfHas": + case "recordOvfKeys": + case "fieldSet": + case "recordSet": + case "recordKeySet": + case "recordKeyDelete": + return same(parent.obj, child); + case "forOf": + return same(parent.iterable, child); + case "arrIntrinsic": + if (same(parent.receiver, child)) return !RECEIVER_RESULT.has(parent.method); + return READ_ARGS.has(parent.method); + case "mapIntrinsic": + if (same(parent.receiver, child)) return true; + return parent.method === "get" || parent.method === "has" || parent.method === "delete"; + case "setIntrinsic": + if (same(parent.receiver, child)) return true; + return parent.method === "has" || parent.method === "delete"; + case "bin": + return parent.op === "===" || parent.op === "!=="; + case "toBool": + case "unary": + case "unionIsTag": + case "unionEq": + return true; + case "unionDisc": + case "unionKeyGet": + return same(parent.value, child); + case "callValue": + return same(parent.callee, child); + case "call": { + const index = indexOfNode(parent.args, child); + return index >= 0 && summaries.get(parent.callee)?.has(index) === true; + } + default: + return false; + } +} + +/** The index of `node` in `list`, or -1. */ +export function indexOfNode(list: readonly IrExpr[], node: Node): number { + for (let i = 0; i < list.length; i++) if (same(list[i], node)) return i; + return -1; +} + +/** The arguments of a node that takes them (calls, constructions, + * intrinsics), or null. */ +export function argsOf(node: Node): readonly IrExpr[] | null { + switch (node.kind) { + case "call": + case "callValue": + case "virtualCall": + case "new": + case "newValue": + case "ffiCall": + case "dynCall": + case "dynInvoke": + case "intrinsic": + case "libCall": + case "arrIntrinsic": + case "bytesIntrinsic": + case "mapIntrinsic": + case "setIntrinsic": + case "strIntrinsic": + case "regexIntrinsic": + return node.args; + default: + return null; + } +} + +const LOOP_STMTS = new Set(["while", "doWhile", "for", "forOf"]); + +/** Calls `visit(node, parent, loops)` for every node of `body`, parents + * before their children (siblings in no set order), where `loops` counts + * the loops around `node`. Allocates nothing per node. */ +export function forEachNode( + body: readonly IrStmt[], + visit: (node: Node, parent: Node | undefined, loops: number) => void, +): void { + const nodes: Node[] = []; + const parents: (Node | undefined)[] = []; + const depths: number[] = []; + let parent: Node | undefined = undefined; + let depth = 0; + const pushExpr = (child: IrExpr): boolean => { + nodes.push(child); + parents.push(parent); + depths.push(depth); + return true; + }; + const pushStmt = (child: IrStmt): boolean => { + nodes.push(child); + parents.push(parent); + depths.push(depth); + return true; + }; + for (const st of body) pushStmt(st); + while (nodes.length > 0) { + const node = nodes.pop()!; + const above = parents.pop(); + const loops = depths.pop()!; + visit(node, above, loops); + parent = node; + depth = loops + (LOOP_STMTS.has(node.kind) ? 1 : 0); + if (isExpr(node)) everyExprChild(node, pushExpr, pushStmt); + else everyStmtChild(node, pushExpr, pushStmt); + } +} diff --git a/packages/compiler/src/backend/llvm/region-sites.test.ts b/packages/compiler/src/backend/llvm/region-sites.test.ts new file mode 100644 index 000000000..97fa94577 --- /dev/null +++ b/packages/compiler/src/backend/llvm/region-sites.test.ts @@ -0,0 +1,223 @@ +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 { IrExpr, IrModule } from "../../ir/ir.js"; +import { analyzeRegionGroups } from "./region-groups.js"; +import { children, type Node } from "./region-ir.js"; +import { planRegionSites } from "./region-sites.js"; + +async function moduleFor(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-region-sites-")); + 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 }); + } +} + +/** The nodes in function `fn` matching `pick`, in order. */ +function nodesIn(mod: IrModule, fn: string, pick: (e: IrExpr) => boolean): IrExpr[] { + const out: IrExpr[] = []; + const visit = (node: Node): void => { + if (pick(node as IrExpr)) out.push(node as IrExpr); + const kids: Node[] = []; + children(node, kids); + for (const k of kids) visit(k); + }; + for (const s of mod.functions.find((f) => f.name === fn)!.body) visit(s); + return out; +} + +const calls = (callee: string) => (e: IrExpr) => e.kind === "call" && e.callee === callee; +const records = (e: IrExpr) => e.kind === "recordLit"; + +function plan(mod: IrModule) { + const groups = analyzeRegionGroups(mod); + const sites = planRegionSites(mod, groups); + return { groups, sites }; +} + +const TREE = ` +class Node { + left: Node | null; + right: Node | null; + constructor(left: Node | null, right: Node | null) { + this.left = left; + this.right = right; + } + count(): number { + return 1 + (this.left === null ? 0 : this.left.count()) + (this.right === null ? 0 : this.right.count()); + } +} +function tree(depth: number): Node { + return depth === 0 ? new Node(null, null) : new Node(tree(depth - 1), tree(depth - 1)); +} +`; + +test("a recursive builder's root takes an arena for every node", async () => { + const mod = await moduleFor(`${TREE} +let total = 0; +for (let d = 0; d < 4; d++) total += tree(d).count(); +console.log(total); +`); + const { groups, sites } = plan(mod); + const roots = nodesIn(mod, "%init.0", calls("tree")).filter((c) => groups.roots.has(c)); + expect(roots.length).toBe(1); + for (const n of nodesIn(mod, "tree", (e) => e.kind === "new")) + expect(sites.arenaSites.has(n)).toBe(true); + for (const c of nodesIn(mod, "tree", calls("tree"))) expect(sites.arenaSites.has(c)).toBe(true); +}); + +test("a builder of a bounded number of objects is no root", async () => { + const mod = await moduleFor(` +interface V { x: number; y: number } +function add(a: V, b: V): V { + return { x: a.x + b.x, y: a.y + b.y }; +} +let at: V = { x: 0, y: 0 }; +let sum = 0; +for (let i = 0; i < 10; i++) { + const step = add(at, { x: i, y: 1 }); + sum += step.x; +} +console.log(sum, at.y); +`); + const { groups } = plan(mod); + expect(groups.roots.size).toBe(0); + expect(groups.calls.size).toBe(0); + expect(groups.reachable.size).toBe(0); +}); + +test("a list a builder appends to in a loop takes the arena", async () => { + const mod = await moduleFor(` +interface Row { id: number; score: number } +function rows(n: number): Row[] { + const out: Row[] = []; + for (let i = 0; i < n; i++) { + const row: Row = { id: i, score: i * 2 }; + out.push(row); + } + return out; +} +let total = 0; +for (let k = 0; k < 3; k++) total += rows(10 + k).length; +console.log(total); +`); + const { groups, sites } = plan(mod); + expect(nodesIn(mod, "%init.0", calls("rows")).some((c) => groups.roots.has(c))).toBe(true); + const [row] = nodesIn(mod, "rows", records); + expect(sites.arenaSites.has(row!)).toBe(true); + const [out] = nodesIn(mod, "rows", (e) => e.kind === "arrayLit"); + expect(sites.arenaSites.has(out!)).toBe(true); +}); + +test("values that replace elements or die in the loop take the heap", async () => { + const mod = await moduleFor(` +interface V { x: number; y: number } +function vec(x: number, y: number): V { + return { x, y }; +} +function add(a: V, b: V): V { + return { x: a.x + b.x, y: a.y + b.y }; +} +function simulate(n: number, steps: number): number { + const points: V[] = []; + for (let i = 0; i < n; i++) points.push(vec(i, i)); + for (let s = 0; s < steps; s++) + for (let i = 0; i < n; i++) points[i] = add(points[i]!, vec(0, 1)); + let sum = 0; + for (const p of points) sum += p.x + p.y; + return sum; +} +console.log(simulate(100, 10)); +`); + const { groups, sites } = plan(mod); + // The list's elements are replaced, so it keeps nothing: no group grows. + expect(groups.calls.size).toBe(0); + expect(groups.roots.size).toBe(0); + expect(sites.arenaSites.size).toBe(0); +}); + +test("a value reassigned in a loop and returned takes the heap", async () => { + const mod = await moduleFor(` +class C { + re: number; + im: number; + constructor(re: number, im: number) { + this.re = re; + this.im = im; + } + times(o: C): C { + return new C(this.re * o.re - this.im * o.im, this.re * o.im + this.im * o.re); + } +} +function power(c: C, n: number): C { + let z = new C(1, 0); + for (let i = 0; i < n; i++) z = z.times(c); + return z; +} +let s = 0; +for (let k = 0; k < 5; k++) s += power(new C(0.5, 0.5), k).re; +console.log(s); +`); + const { groups } = plan(mod); + expect(nodesIn(mod, "%init.0", calls("power")).some((c) => groups.roots.has(c))).toBe(false); +}); + +test("calls in an arena-aware function whose values die take the heap", async () => { + const mod = await moduleFor(` +interface P { x: number; next: P | null } +function weight(p: P): number { + return p.x * 2; +} +function probe(x: number): P { + return { x, next: null }; +} +function chain(n: number): P { + let head: P = { x: 0, next: null }; + const first = head; + for (let i = 1; i < n; i++) { + const node: P = { x: weight(probe(i)), next: null }; + head.next = node; + head = node; + } + return first; +} +let total = 0; +for (let k = 0; k < 3; k++) total += chain(5).x; +console.log(total); +`); + const { groups, sites } = plan(mod); + expect(nodesIn(mod, "%init.0", calls("chain")).some((c) => groups.roots.has(c))).toBe(true); + const [probe] = nodesIn(mod, "chain", calls("probe")); + expect(sites.arenaSites.has(probe!)).toBe(false); + expect(sites.heapCalls.has(probe!)).toBe(true); + for (const node of nodesIn(mod, "chain", records)) expect(sites.arenaSites.has(node)).toBe(true); +}); + +test("a root assigned in a loop to a local declared outside it is no root", async () => { + const mod = await moduleFor(`${TREE} +function run(n: number): number { + let t = tree(1); + let total = t.count(); + for (let d = 0; d < n; d++) { + t = tree(d); + total += t.count(); + } + return total; +} +console.log(run(6)); +`); + const { groups } = plan(mod); + const [first, inLoop] = nodesIn(mod, "run", calls("tree")); + expect(groups.roots.has(first!)).toBe(true); + expect(groups.roots.has(inLoop!)).toBe(false); +}); diff --git a/packages/compiler/src/backend/llvm/region-sites.ts b/packages/compiler/src/backend/llvm/region-sites.ts new file mode 100644 index 000000000..95167524b --- /dev/null +++ b/packages/compiler/src/backend/llvm/region-sites.ts @@ -0,0 +1,349 @@ +/* Which allocations of a region group take its arena (arenas.ts), and + * which roots and call regions are worth one. + * + * An arena pays off for objects that live until the group is dropped: it + * saves their frees and teardown, but holds their memory until the drop. + * A temporary that dies early is cheaper on the heap, which reuses its + * memory at once. So in an arena-aware function an allocation takes the + * arena only when its value is kept by the group (its fate is the arena): + * - it is returned; + * - it is content of a new value that is kept; + * - it is stored, other than by an element write, into the graph of a + * local that is kept (`out.push(row)`, `last.next = member`); + * - it is bound to a local that is kept. + * A local is kept when its value is returned or stored or put into + * something kept; a constructor's `this` is kept (its object follows its + * `new`); and in the callee of a call region, a local declared at the top + * of the body lives until the region drops. An element write (`a[i] = v`) + * usually replaces a value, so its value takes the heap. + * + * Every other allocation takes the heap, and so does everything a call + * allocates when the call's value is not kept: the emitter suspends the + * arena around such a call (heapCalls). Heap objects are always safe in a + * group; only arena objects need the group's proof. + * + * A root or call region is worth an arena when its group grows: a kept + * allocation runs in a loop, or a kept value comes from a recursive call + * or from a call of a function whose group grows. Other groups are small, + * and opening and dropping their arenas costs more than their frees. */ +import type { IrFunction, IrModule } from "../../ir/ir.js"; +import { contentOf, source, storeOf, storeTarget, type RegionGroups } from "./region-groups.js"; +import { + argsOf, + children, + indexOfNode, + isExpr, + refType, + same, + transparent, + type Node, +} from "./region-ir.js"; +import { VirtualTargets } from "./reference-effects.js"; + +export interface RegionSites { + /** Allocation sites (and calls) whose value the group keeps. */ + arenaSites: Set; + /** Calls, constructions and closure calls in arena-aware functions whose + * value the group does not keep and whose callee may allocate: the + * arena is suspended while they run. */ + heapCalls: Set; +} + +const LOOPS = new Set(["while", "doWhile", "for", "forOf"]); + +/** Collection methods that remove what their receiver holds. */ +const REMOVALS = new Set(["pop", "shift", "splice", "delete", "clear"]); + +/** The local whose graph `node` drops values from (an element write, a + * length write, a delete, a removing method), or null. */ +function removalTarget(node: Node): string | null { + switch (node.kind) { + case "arraySet": + case "arraySetLength": + case "arraySetUndefined": + case "arrayDelete": + return storeTarget(node.arr); + case "recordKeyDelete": + return storeTarget(node.obj); + case "arrIntrinsic": + case "mapIntrinsic": + case "setIntrinsic": + return REMOVALS.has(node.method) ? storeTarget(node.receiver) : null; + default: + return null; + } +} + +/** The functions a call-like node may run, or null when unknown. */ +function targetsOf(vt: VirtualTargets, node: Node): readonly string[] | null { + if (node.kind === "call") return [node.callee]; + if (node.kind === "new") return [`%${node.className}.constructor`]; + if (node.kind === "virtualCall") { + const t = vt.targets(node.className, node.method); + return t !== null && t.length > 0 ? t : null; + } + return null; +} + +const callLike = (node: Node): boolean => + node.kind === "call" || + node.kind === "new" || + node.kind === "virtualCall" || + node.kind === "callValue" || + node.kind === "newValue"; + +/** The functions that run inside the arenas of `roots`: what they call, + * transitively, with the closures they pass. */ +export function reachableFrom(mod: IrModule, roots: Iterable): Set { + const fns = new Map(mod.functions.map((f) => [f.name, f])); + const vt = new VirtualTargets(mod.classes ?? []); + const reachable = new Set(); + const queue: string[] = []; + const visit = (node: Node): void => { + const reach = (name: string): void => { + if (fns.has(name) && !reachable.has(name)) { + reachable.add(name); + queue.push(name); + } + }; + if (node.kind === "closure") reach(node.fnName); + else for (const t of targetsOf(vt, node) ?? []) reach(t); + const kids: Node[] = []; + children(node, kids); + for (const k of kids) visit(k); + }; + for (const r of roots) visit(r); + while (queue.length > 0) for (const st of fns.get(queue.pop()!)!.body) visit(st); + return reachable; +} + +interface FnSites { + /** Kept call-like nodes and allocations, with whether each is in a loop. */ + kept: Map; + /** Call-like nodes whose value is not kept. */ + dropped: Node[]; +} + +/** Classifies the allocations and calls of `fn`. */ +function classify(fn: IrFunction, topLocalsKept: boolean): FnSites { + const locals = new Set(fn.locals.map((l) => l.id)); + // Alias classes: a local bound to another local shares its fate. + const parent = new Map(); + const find = (id: string): string => { + let r = id; + while (parent.has(r)) r = parent.get(r)!; + return r; + }; + const unite = (a: string, b: string): void => { + const ra = find(a), + rb = find(b); + if (ra !== rb) parent.set(ra, rb); + }; + const kept = new Set(); + const keep = (id: string): boolean => { + const r = find(id); + if (kept.has(r)) return false; + kept.add(r); + return true; + }; + const isKept = (id: string): boolean => kept.has(find(id)); + // Kept from the start (applied once the alias classes are known). + const seeds: string[] = []; + if (fn.name.endsWith(".constructor") && fn.params.length > 0) seeds.push(fn.params[0]!.localId); + if (topLocalsKept) { + const types = new Map(fn.locals.map((l) => [l.id, l.type])); + for (const st of fn.body) + if (st.kind === "varDecl" && refType(types.get(st.localId) ?? { kind: "void" })) + seeds.push(st.localId); + } + + // Uses of locals and of value-producing nodes, recorded once. + type Fate = + | { kind: "kept" } + | { kind: "local"; id: string } + | { kind: "target"; id: string } + | { kind: "node"; node: Node } + | { kind: "heap" }; + const fateOf = (node: Node, parents: readonly Node[]): Fate => { + let value: Node = node; + let at = parents.length - 1; + while (at >= 0 && transparent(value, parents[at])) value = parents[at--]!; + const p = at >= 0 ? parents[at] : undefined; + if (p === undefined) return { kind: "heap" }; + if (p.kind === "return") return { kind: "kept" }; + if ( + (p.kind === "varDecl" && p.init !== null && same(p.init, value)) || + (p.kind === "assign" && same(p.value, value)) + ) { + const id = p.localId; + // A local assigned in a loop usually drops its old value. + if (!locals.has(id) || (p.kind === "assign" && depth > 0)) return { kind: "heap" }; + return { kind: "local", id }; + } + const target = storeOf(value, p); + if (target !== undefined) { + if (p.kind === "arraySet") return { kind: "heap" }; + const id = storeTarget(target); + return id !== null && locals.has(id) ? { kind: "target", id } : { kind: "heap" }; + } + if (isExpr(p) && contentOf(value, p)) return { kind: "node", node: p }; + // An argument of a call that returns a reference follows the call's + // value: the callee may put it into its result. + const args = argsOf(p); + if ( + callLike(p) && + isExpr(p) && + refType(p.type) && + args !== null && + indexOfNode(args, value) >= 0 + ) + return { kind: "node", node: p }; + return { kind: "heap" }; + }; + + // Lossy locals: their graphs drop values (element writes, removals), so + // what is stored into them is not kept. + const lossy = new Set(); + const fates = new Map(); + const loops = new Map(); + const localUses: [string, Fate][] = []; + const parents: Node[] = []; + let depth = 0; + const walk = (node: Node): void => { + const removal = removalTarget(node); + if (removal !== null) lossy.add(removal); + if (source(node) || callLike(node)) { + fates.set(node, fateOf(node, parents)); + loops.set(node, depth > 0); + } else if (node.kind === "varRef" && locals.has(node.localId) && refType(node.type)) { + const f = fateOf(node, parents); + if (f.kind !== "heap") localUses.push([node.localId, f]); + } + const loop = LOOPS.has(node.kind); + if (loop) depth++; + const kids: Node[] = []; + children(node, kids); + parents.push(node); + for (const k of kids) walk(k); + parents.pop(); + if (loop) depth--; + }; + for (const st of fn.body) walk(st); + + // A local bound to another local joins its class. + for (const [id, f] of localUses) if (f.kind === "local") unite(id, f.id); + for (const id of [...lossy]) lossy.add(find(id)); + for (const id of seeds) keep(id); + const keptFate = (f: Fate, seen = 0): boolean => { + if (seen > 64) return false; + switch (f.kind) { + case "kept": + return true; + case "local": + return isKept(f.id); + case "target": + return isKept(f.id) && !lossy.has(find(f.id)); + case "node": { + const g = fates.get(f.node); + return g !== undefined && keptFate(g, seen + 1); + } + case "heap": + return false; + } + }; + for (let changed = true; changed;) { + changed = false; + for (const [id, f] of localUses) + if (f.kind !== "local" && keptFate(f) && keep(id)) changed = true; + } + const out: FnSites = { kept: new Map(), dropped: [] }; + for (const [e, f] of fates) + if (keptFate(f)) out.kept.set(e, loops.get(e)!); + else if (callLike(e)) out.dropped.push(e); + return out; +} + +/** + * Narrows `groups` to the roots and call regions worth an arena (with + * `reachable` recomputed for them) and classifies the allocations of the + * functions that run in their arenas. + */ +export function planRegionSites(mod: IrModule, groups: RegionGroups): RegionSites { + const fns = new Map(mod.functions.map((f) => [f.name, f])); + const vt = new VirtualTargets(mod.classes ?? []); + const regionCallees = new Set(); + for (const c of groups.calls) if (c.kind === "call") regionCallees.add(c.callee); + const sites = new Map(); + for (const name of groups.reachable) { + const fn = fns.get(name); + if (fn !== undefined) sites.set(name, classify(fn, regionCallees.has(name))); + } + // Which functions reach which (for recursion), over the reachable ones. + const calleesOf = (name: string): Set => { + const out = new Set(); + const s = sites.get(name); + if (s === undefined) return out; + for (const e of [...s.kept.keys(), ...s.dropped]) + for (const t of targetsOf(vt, e) ?? []) if (sites.has(t)) out.add(t); + return out; + }; + const edges = new Map([...sites.keys()].map((n) => [n, calleesOf(n)])); + const reaches = (from: string, to: string): boolean => { + const seen = new Set(); + const stack = [from]; + while (stack.length > 0) { + const n = stack.pop()!; + if (n === to) return true; + if (seen.has(n)) continue; + seen.add(n); + for (const m of edges.get(n) ?? new Set()) stack.push(m); + } + return false; + }; + const grows = new Set(); + for (let changed = true; changed;) { + changed = false; + for (const [name, s] of sites) { + if (grows.has(name)) continue; + let g = false; + for (const [e, inLoop] of s.kept) { + if (inLoop) g = true; + for (const t of targetsOf(vt, e) ?? []) if (grows.has(t) || reaches(t, name)) g = true; + if (g) break; + } + if (g) { + grows.add(name); + changed = true; + } + } + } + const worth = (r: Node): boolean => { + // Module code runs once, and its groups hold the program's own state. + if (r.kind === "call") return grows.has(r.callee) && !r.callee.startsWith("%init."); + if (r.kind !== "new") return false; + if (grows.has(`%${r.className}.constructor`)) return true; + return r.args.some((a) => a.kind === "call" && grows.has(a.callee)); + }; + for (const r of [...groups.roots]) + if (!worth(r)) { + groups.roots.delete(r); + groups.drops.delete(r); + } + for (const c of [...groups.calls]) if (!worth(c)) groups.calls.delete(c); + groups.reachable = reachableFrom(mod, [...groups.roots, ...groups.calls]); + + const arenaSites = new Set(); + const heapCalls = new Set(); + const mayAllocate = (e: Node): boolean => { + const t = targetsOf(vt, e); + return t === null || t.some((n) => groups.allocates.has(n) || !fns.has(n)); + }; + for (const name of groups.reachable) { + const s = sites.get(name); + if (s === undefined) continue; + for (const e of s.kept.keys()) arenaSites.add(e); + for (const e of s.dropped) + if (!groups.roots.has(e) && !groups.calls.has(e) && mayAllocate(e)) heapCalls.add(e); + } + return { arenaSites, heapCalls }; +} diff --git a/packages/compiler/src/backend/llvm/repr-report.test.ts b/packages/compiler/src/backend/llvm/repr-report.test.ts new file mode 100644 index 000000000..ec7b9b5ed --- /dev/null +++ b/packages/compiler/src/backend/llvm/repr-report.test.ts @@ -0,0 +1,65 @@ +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, deserializeModule } from "../../index.js"; +import { emitLlvmModule } from "./emitter.js"; +import { REPR_REPORT_HEADER } from "./repr-report.js"; + +const saved = process.env["SCRIPTC_REPR_REPORT"]; +afterEach(() => { + if (saved === undefined) delete process.env["SCRIPTC_REPR_REPORT"]; + else process.env["SCRIPTC_REPR_REPORT"] = saved; +}); + +test("the representation report names each slot's storage and why a number stays a double", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-repr-report-")); + try { + const entry = join(dir, "main.ts"), + outPath = join(dir, "main.ir.json"), + report = join(dir, "repr.tsv"); + await writeFile( + entry, + ` +class Box { + id: number; + hits: number; + items: number[]; + constructor(id: number, items: number[]) { this.id = id | 0; this.hits = 0; this.items = items; } +} +function scale(value: number, by: number = 2): number { return (value * by) | 0; } +function half(value: number): number { return value / 2; } +const boxes: Box[] = []; +for (let i = 0; i < 4; i++) boxes.push(new Box(i, [i, i + 1])); +for (const b of boxes) b.hits++; +console.log(scale(boxes[1]!.id), scale(3, 4), half(boxes.length), boxes[2]!.items.length, boxes[3]!.hits); +`, + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + process.env["SCRIPTC_REPR_REPORT"] = report; + emitLlvmModule(mod); + const lines = (await readFile(report, "utf8")).trimEnd().split("\n"); + expect(lines[0]).toBe(REPR_REPORT_HEADER); + const rows = lines.slice(1).map((line) => line.split("\t")); + const row = (kind: string, owner: string, name: string) => + rows.find((r) => r[0] === kind && r[1] === owner && r[2] === name); + expect(row("field", "Box", "id")?.slice(3, 5)).toEqual(["f64", "i32"]); + // `++` on a field keeps it a double, and the report says so. + expect(row("field", "Box", "hits")?.[4]).toBe("double"); + expect(row("field", "Box", "hits")?.[5]).not.toBe(""); + expect(row("field", "Box", "items")?.slice(3, 5)).toEqual(["array", "ptr"]); + expect(row("param", "scale", "value")?.[4]).toBe("i32"); + // A defaulted parameter is a `number | undefined` box; the variant for + // calls that pass a number takes an i32. + expect(row("param", "scale", "by")?.slice(3, 5)).toEqual(["f64 | undefined", "box"]); + expect(row("param", "scale.nativeNumber", "by")?.[4]).toBe("i32"); + // An array length may exceed the int32 range. + expect(row("param", "half", "value")?.[4]).toBe("double"); + expect(row("param", "half", "value")?.[5]).toMatch(/^argument in .* is not int32$/); + expect(row("return", "half", "")?.slice(4)).toEqual(["double", "return is not int32"]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/backend/llvm/repr-report.ts b/packages/compiler/src/backend/llvm/repr-report.ts new file mode 100644 index 000000000..8d6e48f30 --- /dev/null +++ b/packages/compiler/src/backend/llvm/repr-report.ts @@ -0,0 +1,141 @@ +/* The representation report (SCRIPTC_REPR_REPORT=): after a module is + * emitted, one tab-separated line per class field, function parameter, + * function result and union, naming the storage the backend chose for it + * and, for numbers kept as doubles, the reason the int32 analysis recorded + * (int32-slots.ts Int32Slots.reasons). It is a diagnostic for ranking + * representation work; nothing reads it back. */ +import { writeFileSync } from "node:fs"; +import { typeKey, type IrFunction, type IrModule, type IrType } from "../../ir/ir.js"; +import type { LlEmitter } from "./emitter.js"; +import { classFieldTypes } from "./classes.js"; +import { INLINE_UNION_FIELD } from "./inline-unions.js"; + +export const REPR_REPORT_HEADER = "kind\towner\tname\ttype\tstorage\treason"; + +/** The file SCRIPTC_REPR_REPORT names, or null. A build that writes the + * report skips the executable cache, whose hit would emit no module. */ +export function representationReportPath(env: NodeJS.ProcessEnv = process.env): string | null { + const path = env["SCRIPTC_REPR_REPORT"]; + return path === undefined || path === "" ? null : path; +} + +/** Writes the report of `emitter`'s module when SCRIPTC_REPR_REPORT names a + * file. */ +export function writeRepresentationReport(emitter: LlEmitter): void { + const path = representationReportPath(); + if (path !== null) writeFileSync(path, representationReport(emitter)); +} + +export function representationReport(emitter: LlEmitter): string { + const mod = emitter.mod; + const slots = emitter.int32Slots; + const lines = [REPR_REPORT_HEADER]; + const row = (...cells: string[]): void => { + lines.push(cells.map((cell) => cell.replace(/[\t\n]/g, " ")).join("\t")); + }; + const typeLabel = typeLabeler(mod); + // A union value that is not a plain box: a nullable or shape pointer. + const unionStorage = (t: IrType): string | null => { + if (t.kind !== "union") return null; + return emitter.nullableUnions.get(t.unionId)?.repr ?? "box"; + }; + + for (const cls of mod.classes ?? []) { + const meta = emitter.classMeta.get(cls.name); + const types = classFieldTypes(emitter.shapeHost, cls, slots); + // Inherited fields come first in def.fields: report each field once, + // on the class that declares it. + const inherited = meta?.base?.def.fields.length ?? 0; + cls.fields.forEach((field, i) => { + if (i < inherited) return; + let storage = types[i]!; + if (storage === INLINE_UNION_FIELD) storage = "inline union"; + else if (meta?.compressed[i] === true) storage = "ref32"; + else if (storage === "ptr") storage = unionStorage(field.type) ?? "ptr"; + let reason = ""; + if (field.type.kind === "f64" && storage === "double") { + const family = slots.family(cls.name, field.name); + reason = + family === null + ? "no field family" + : (slots.reasons.get(`field:${family}`) ?? "not a candidate"); + } + row("field", cls.name, field.name, typeLabel(field.type), storage, reason); + }); + } + + for (const fn of mod.functions) { + const abi = emitter.int32Abis.get(fn.name); + fn.params.forEach((param, i) => { + let storage: string; + let reason = ""; + if (param.type.kind === "f64") { + storage = abi?.params[i] === true ? "i32" : "double"; + if (storage === "double") reason = parameterReason(emitter, fn, param.localId); + } else storage = unionStorage(param.type) ?? scalarStorage(param.type); + row("param", fn.name, param.name, typeLabel(param.type), storage, reason); + }); + const ret = fn.returnType; + if (ret.kind === "void") continue; + let storage: string; + let reason = ""; + if (ret.kind === "f64") { + storage = abi?.result === true ? "i32" : "double"; + if (storage === "double") + reason = slots.reasons.get(`ret:${fn.name}`) ?? notCandidate(emitter, fn); + } else if (ret.kind === "union" && abi?.union === true) storage = "register pair"; + else storage = unionStorage(ret) ?? scalarStorage(ret); + row("return", fn.name, "", typeLabel(ret), storage, reason); + } + + for (const def of mod.unions ?? []) + row( + "union", + def.id, + "", + typeLabel({ kind: "union", unionId: def.id }), + unionStorage({ kind: "union", unionId: def.id })!, + "", + ); + return lines.join("\n") + "\n"; +} + +function scalarStorage(t: IrType): string { + switch (t.kind) { + case "f64": + return "double"; + case "bool": + return "i1"; + default: + return "ptr"; + } +} + +function parameterReason(emitter: LlEmitter, fn: IrFunction, localId: string): string { + const slots = emitter.int32Slots; + const recorded = slots.reasons.get(`param:${fn.name}\0${localId}`); + if (recorded !== undefined) return recorded; + // Proven, but the function has no i32 entry (findInt32Abis). + if (slots.hasParam(fn.name, localId)) return "no i32 entry"; + return notCandidate(emitter, fn); +} + +/** Why a function's numbers were never int32 candidates: the analysis only + * proves functions that direct calls alone reach. */ +function notCandidate(emitter: LlEmitter, fn: IrFunction): string { + if (fn.async) return "async function"; + if (fn.generator) return "generator"; + if (fn.captures !== undefined) return "closure"; + if (!emitter.int32Slots.called.has(fn.name)) return "no direct call"; + return "escapes or is dispatched"; +} + +/** Type labels with union arms spelled out. */ +function typeLabeler(mod: IrModule): (t: IrType) => string { + const arms = new Map((mod.unions ?? []).map((def) => [def.id, def.arms])); + return (t) => { + if (t.kind !== "union") return typeKey(t); + const list = arms.get(t.unionId); + return list === undefined ? typeKey(t) : list.map(typeKey).join(" | "); + }; +} diff --git a/packages/compiler/src/backend/llvm/runtime-registries.test.ts b/packages/compiler/src/backend/llvm/runtime-registries.test.ts new file mode 100644 index 000000000..a704deb61 --- /dev/null +++ b/packages/compiler/src/backend/llvm/runtime-registries.test.ts @@ -0,0 +1,100 @@ +import { readFileSync } from "node:fs"; +import { expect, test } from "vitest"; +import { BYTES_ELEM_NUM, CLOSURE_KIND, SCR_ARR_FROZEN, SCR_ARR_STATES } from "./common.js"; +import { DYN_KIND, MESSAGE_KIND } from "./dyn.js"; + +// The backend writes the numbers of several runtime enums and flag sets +// into the IR it emits. Each check here reads the C declaration and fails +// when the backend's copy disagrees, or when two names share a value. +const source = (file: string) => + readFileSync(new URL(`../../../../runtime/src/${file}`, import.meta.url), "utf8") + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, ""); + +/** The members of `typedef enum ... { ... } ;` with their values. */ +function cEnum(file: string, name: string): Map { + const match = new RegExp(`typedef enum(?: \\w+)? \\{([^}]*)\\} ${name};`).exec(source(file)); + expect(match, `${name} in ${file}`).not.toBeNull(); + const body = match![1]!; + const members = new Map(); + let next = 0; + for (const entry of body + .split(",") + .map((e) => e.trim()) + .filter(Boolean)) { + const [, member, value] = /^(\w+)(?:\s*=\s*(\d+))?$/.exec(entry)!; + const v = value === undefined ? next : Number(value); + members.set(member!, v); + next = v + 1; + } + return members; +} + +/** The `#define NAME u` flags of a header. */ +function cFlags(file: string, prefix: string): Map { + const re = new RegExp(`^#define ${prefix}(\\w+) (\\d+)u\\b`, "gm"); + return new Map([...source(file).matchAll(re)].map(([, name, value]) => [name!, Number(value)])); +} + +function expectDistinct(values: Map, what: string): void { + const owners = new Map(); + for (const [name, value] of values) { + expect(owners.get(value), `${what}: ${name} = ${value}`).toBeUndefined(); + owners.set(value, name); + } +} + +function expectSingleBits(values: Map, what: string): void { + for (const [name, value] of values) { + expect(value > 0 && (value & (value - 1)) === 0, `${what}: ${name} = ${value}`).toBe(true); + } + expectDistinct(values, what); +} + +test("DYN_KIND matches ScrDynKind", () => { + const kinds = cEnum("scr_dyn.h", "ScrDynKind"); + expectDistinct(kinds, "ScrDynKind"); + for (const [name, value] of Object.entries(DYN_KIND)) { + expect(kinds.get(`SCR_DYN_${name}`), name).toBe(value); + } +}); + +test("MESSAGE_KIND matches ScrMessageKind", () => { + const kinds = cEnum("scr_dyn.h", "ScrMessageKind"); + expectDistinct(kinds, "ScrMessageKind"); + for (const [name, value] of Object.entries(MESSAGE_KIND)) { + expect(kinds.get(`SCR_MESSAGE_${name}`), name).toBe(value); + } +}); + +test("BYTES_ELEM_NUM matches ScrBytesElem", () => { + const elems = cEnum("scr_bytes.h", "ScrBytesElem"); + expectDistinct(elems, "ScrBytesElem"); + expect(Object.keys(BYTES_ELEM_NUM).length).toBe(elems.size); + for (const [name, value] of Object.entries(BYTES_ELEM_NUM)) { + expect(elems.get(`SCR_BYTES_${name.toUpperCase()}`), name).toBe(value); + } +}); + +test("CLOSURE_KIND matches the SCR_CLOSURE_KIND_* bits", () => { + const flags = cFlags("scr_closure.h", "SCR_CLOSURE_KIND_"); + expect(flags.get("CALL_MASK")).toBe(CLOSURE_KIND.GENERATOR | CLOSURE_KIND.ASYNC); + flags.delete("CALL_MASK"); + expect(flags.get("MASK")).toBe(CLOSURE_KIND.RECEIVER_FREE - 1); + flags.delete("MASK"); + expectSingleBits(flags, "SCR_CLOSURE_KIND"); + expect(Object.fromEntries(flags)).toEqual(CLOSURE_KIND); +}); + +test("the %ScrArr spare flags the backend tests match scr_array.h", () => { + const flags = cFlags("scr_array.h", "SCR_ARR_"); + const spare = new Map( + ["INLINE_SLOTS", "HEAD_ROOM", "UNSHIFTED", "HAS_EXT", "STATES", "FROZEN"].map((name) => [ + name, + flags.get(name)!, + ]), + ); + expectSingleBits(spare, "%ScrArr spare"); + expect(spare.get("STATES")).toBe(SCR_ARR_STATES); + expect(spare.get("FROZEN")).toBe(SCR_ARR_FROZEN); +}); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 79a79f36c..25afbad69 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -2,6 +2,7 @@ * Containers store erased adapters; shape helpers retain, release and trace * their fields. Every allocation has a size_t reference count at offset zero * and participates in the runtime allocation audit. */ +import type { HeapCensus } from "./heap-census.js"; import type { IrClassDef, IrModule, IrRecordShape, IrType } from "../../ir/ir.js"; import { isIdentityCollectionKey, @@ -35,6 +36,7 @@ import { emitObjectAlloc, emitObjectFree, emitSegmentFree, + type FreeObservers, CYC_APPEND, cycleAppendDecls, cycleAppendHelper, @@ -59,6 +61,14 @@ import { type InlineUnionFields, } from "./inline-unions.js"; import { JS_BITS, JS_TAGS } from "./jsval-encoding.js"; +import { + ARENA_RC, + arenaDisposeLines, + arenaDisposeSym, + arenaNewLines, + disposeFieldLines, + plainRefType, +} from "./arenas.js"; import { recordMayKeepSource } from "./record-sources.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -69,6 +79,8 @@ const CYC_MAX_TYPES = 32768; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { + /** The counters of a build instrumented for the heap census (heap-census.ts). */ + readonly heapCensus?: HeapCensus | null; /** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built, * opened, used and entry. */ journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined; @@ -136,6 +148,13 @@ export interface ShapeHost { * allocate in regions and count references in their region chunk instead * of being empty (llvm/regions.ts). */ regions?: RegionHooks | null; + /** Record shapes and classes with an arena-aware constructor + * (arenas.ts). */ + arenaShapes?: ReadonlySet; + arenaClasses?: ReadonlySet; + /** Every arena the program opens is closed (ArenaPlan.allClosed): the + * arena-aware constructors take no dispose list. */ + arenasClosed?: boolean; /** The target C ABI's `size_t` / refcount integer type. */ readonly sizeType: "i32" | "i64"; /** Worker executables: the inline allocator state is thread-local. */ @@ -1197,8 +1216,37 @@ export function arrNewCall( /** An empty array expected to receive a few elements (its first slots * live in its own block): capText must be "0". */ small = false, + /** In the current group arena when one is open (arenas.ts). */ + arena = false, ): string { - const useRef = + const r = arena ? "_r" : ""; + const useRef = arrUsesRef(host, elem); + if (!useRef) { + if (small) { + host.declare(`declare ptr @scr_arr_new_small${r}(i32)`); + return `call ptr @scr_arr_new_small${r}(i32 ${elemKindNum(elem)})`; + } + host.declare(`declare ptr @scr_arr_new${r}(i32, ${host.sizeType})`); + return `call ptr @scr_arr_new${r}(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`; + } + if (small) { + host.declare(`declare ptr @scr_arr_new_ops_small${r}(ptr)`); + return `call ptr @scr_arr_new_ops_small${r}(ptr ${arrOps(host, elem)})`; + } + host.declare(`declare ptr @scr_arr_new_ops${r}(ptr, ${host.sizeType})`); + return `call ptr @scr_arr_new_ops${r}(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" || @@ -1215,21 +1263,8 @@ export function arrNewCall( elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) elem.kind === "date" || // Date objects: scr_date_* 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 === "array" && traceAdapter(host, elem) !== null) + ); } /** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF @@ -1807,6 +1842,115 @@ 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. @@ -1825,6 +1969,7 @@ export function releaseBody( 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)`); @@ -1832,7 +1977,9 @@ export function releaseBody( // 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, segmented ? host : null) : []), + ...(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:`, @@ -2128,6 +2275,11 @@ export function emitRecordShapes( ), ); 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, @@ -2140,6 +2292,16 @@ export function emitRecordShapes( chained, segmented, shape.freezable === true, + iterative + ? iterativeTeardownDefinitions( + host, + destroy, + struct, + refMembers.map((m) => m.index), + `@${mangleRecordRelease(shape.id)}`, + observers, + ) + : null, ), ``, ); @@ -2186,6 +2348,35 @@ export function emitRecordShapes( if (traced) nw.push(...youngEntryLines(host)); nw.push(` ret ptr %o`, `}`, ``); defs.push(...nw); + if (host.arenaShapes?.has(shape.id) === true && !shape.indexValue) { + const newSym = mangleRecordNew(shape.id); + const dispose = + refMembers.length > 0 && host.arenasClosed !== true ? arenaDisposeSym(newSym) : null; + const init = [` store ${host.sizeType} ${ARENA_RC}, ptr %o`]; + if (shapeNumber !== 0) + init.push( + ` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`, + ` store i32 ${shapeNumber}, ptr %shp ; shape number`, + ); + defs.push( + ...arenaNewLines(host, newSym, sizeOf, traced ? cycType : null, dispose, init, "%o"), + ); + if (dispose !== null) + defs.push( + ...arenaDisposeLines( + newSym, + refMembers.flatMap((m, i) => + disposeFieldLines( + i, + `getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, + plainRefType(m.type), + releaseSym(host, m.type), + llvmCommentText(m.name), + ), + ), + ), + ); + } if (host.recordCloneShapes.has(shape.id)) { const attrs = shape.fields.length >= 16 ? "#2" : FN_ATTRS; diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index ae2b904db..36793598b 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -57,8 +57,9 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO 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)); + // caller's storage, unless the call roots a group: then the result goes + // in the group's arena with the objects it holds (arenas.ts). + if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)) && !host.opensArena(e); if (e.kind === "recordLit") { if (e.type.kind !== "record") return false; const shape = host.recordsById.get(e.type.shapeId); diff --git a/packages/compiler/src/backend/llvm/unwind-abi.test.ts b/packages/compiler/src/backend/llvm/unwind-abi.test.ts index 5f7f5895b..bcef1c379 100644 --- a/packages/compiler/src/backend/llvm/unwind-abi.test.ts +++ b/packages/compiler/src/backend/llvm/unwind-abi.test.ts @@ -283,3 +283,43 @@ test("calls that cannot unwind are marked nounwind", () => { 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 index 4dc611913..821c5351b 100644 --- a/packages/compiler/src/backend/llvm/unwind-abi.ts +++ b/packages/compiler/src/backend/llvm/unwind-abi.ts @@ -2,6 +2,7 @@ * 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; @@ -129,13 +130,14 @@ export function unwindAbiFunctions( } // 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 noteNames = (node: object, skip: string | undefined): void => { - for (const key in node) { - if (key === skip) continue; - const value = (node as Record)[key]; - if (typeof value === "string" && names.has(value)) referenced.add(value); - } + 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 @@ -171,10 +173,10 @@ export function unwindAbiFunctions( // 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); - noteNames(e, "callee"); + eachStringField(e, noteCallName); return true; } - noteNames(e, undefined); + 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") { @@ -193,7 +195,7 @@ export function unwindAbiFunctions( stmt: (s) => { const kind = s.kind as string; if (kind === "throw" || kind === "rethrow") throws.add(fn.name); - noteNames(s, undefined); + eachStringField(s, noteName); return true; }, }); @@ -301,6 +303,46 @@ function markNounwind(line: string): string { : `${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 { @@ -318,7 +360,7 @@ function dummyValue(type: string): string { * these functions that emitDirectCall lowered are invokes already. */ export function bridgePlainUnwindCalls(parts: string[]): string[] { const raisers = new Set(); - const split = parts.map((part) => part.split(/\n(?=define )/)); + const split = parts.map(splitFunctions); for (const chunks of split) for (const chunk of chunks) { const m = DEFINE_NAME.exec(chunk); @@ -359,20 +401,29 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] { } } 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 (const line of chunk.split("\n")) { - if (line.startsWith("define ")) continue; - for (const m of line.matchAll(/@([\w.$%-]+)/g)) { - if (!raisers.has(m[1]!)) continue; - const at = m.index!; - if ( - /(?:call|invoke) [^@]*$/.test(line.slice(0, at)) && - line.charAt(at + m[0].length) === "(" - ) - continue; - violations.add(m[1]!); + 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; @@ -381,6 +432,34 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] { 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[] = []; diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts index 49b3dde8b..7c421175c 100644 --- a/packages/compiler/src/backend/llvm/wrapping-sums.ts +++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts @@ -2,7 +2,7 @@ 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, @@ -72,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) { @@ -182,6 +188,50 @@ export function emitWrappingSum( 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 diff --git a/packages/compiler/src/backend/loop-polls.ts b/packages/compiler/src/backend/loop-polls.ts index 866711fb7..b49026fa2 100644 --- a/packages/compiler/src/backend/loop-polls.ts +++ b/packages/compiler/src/backend/loop-polls.ts @@ -137,3 +137,18 @@ export function loopAccessesTypedArrays(loop: Loop): boolean { }; return loop.body.some(inStmt); } + +/** Whether `body` holds a loop outside its closures. */ +export function containsLoop(body: readonly 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); +} diff --git a/packages/compiler/src/backend/may-throw.test.ts b/packages/compiler/src/backend/may-throw.test.ts index 0a950c2cc..1bbbdb299 100644 --- a/packages/compiler/src/backend/may-throw.test.ts +++ b/packages/compiler/src/backend/may-throw.test.ts @@ -406,7 +406,7 @@ test("worker programs poll for termination only where execution can be unbounded expect(computeMayThrow({ ...mod, workers: false }).workerEntryPolls).toBeUndefined(); }); -test("stack checks cover recursive components and recursion through reentrant operations", () => { +test("stack checks cover every recursive cycle, including recursion through reentrant operations", () => { const mod = moduleWith( fn("caller", [call("self"), call("left"), call("leaf"), call("walker")], []), fn("self", [call("self"), call("leaf")], []), @@ -420,7 +420,12 @@ test("stack checks cover recursive components and recursion through reentrant op fn("visitor", [callClosure("leaf")], []), ); const answer = computeMayThrow(mod, { stackChecks: true }); - expect([...answer.stackChecks!].sort()).toEqual(["left", "right", "self", "step", "walker"]); + // One check per cycle: self, one of left and right, and one of walker and + // step (the earlier function on ties). + expect([...answer.stackChecks!].sort()).toEqual(["left", "self", "walker"]); + expect( + [...computeMayThrow(mod, { stackChecks: "everyCycleMember" }).stackChecks!].sort(), + ).toEqual(["left", "right", "self", "step", "walker"]); // Checked functions throw the overflow RangeError, so their callers poll; // a throwing closure target makes every indirect call poll as well. expect(answer.indirect).toBe(true); @@ -437,7 +442,7 @@ test("stack checks cover recursive components and recursion through reentrant op expect(computeMayThrow(mod).fns.size).toBe(0); }); -test("stack-checked synchronous functions skip the worker entry poll", () => { +test("synchronous functions on a checked cycle skip the worker entry poll", () => { const asyncSelf: IrFunction = { ...fn("asyncSelf", [call("asyncSelf")], []), async: true }; const mod: IrModule = { ...moduleWith( @@ -452,10 +457,11 @@ test("stack-checked synchronous functions skip the worker entry poll", () => { workers: true, }; const answer = computeMayThrow(mod, { stackChecks: true }); - expect([...answer.stackChecks!].sort()).toEqual(["asyncSelf", "caller", "left", "right", "self"]); - // Callers test after every call of a stack-checked function, and its - // overflow unwinds through those tests, so only the module entry, async - // bodies and unbounded functions without a stack check poll on entry. + expect([...answer.stackChecks!].sort()).toEqual(["asyncSelf", "caller", "left", "self"]); + // Callers test after every call of a function on a checked cycle (it may + // throw), and an overflow unwinds through those tests, so only the module + // entry, async bodies and unbounded functions off any cycle poll on entry; + // right needs no poll although left holds the cycle's check. expect([...answer.workerEntryPolls!].sort()).toEqual(["asyncSelf", "caller", "walker"]); expect(answer.fns.has("self") && answer.fns.has("left") && answer.fns.has("right")).toBe(true); // Without stack checks (WASI, Windows, libraries) every cycle keeps its poll. @@ -469,6 +475,48 @@ test("stack-checked synchronous functions skip the worker entry poll", () => { ]); }); +test("stack checks meet every cycle once and bound the runs between checks", () => { + // A ring of 80 functions, with a chord from fn10 back to fn0, and a + // separate ring of three whose functions also call each other backwards. + const ring = Array.from({ length: 80 }, (_, i) => + fn(`fn${i}`, i === 10 ? [call("fn11"), call("fn0")] : [call(`fn${(i + 1) % 80}`)], []), + ); + const mod = moduleWith( + ...ring, + fn("a", [call("b"), call("c")], []), + fn("b", [call("c"), call("a")], []), + fn("c", [call("a"), call("b")], []), + ); + const checks = computeMayThrow(mod, { stackChecks: true }).stackChecks!; + // Removing the checked functions leaves no cycle, and no run of more than + // 32 unchecked calls. + const callees = new Map( + mod.functions.map((f) => [ + f.name, + f.body.flatMap((s) => + s.kind === "exprStmt" && s.expr.kind === "call" ? [s.expr.callee] : [], + ), + ]), + ); + const longest = new Map(); + const visiting = new Set(); + const run = (name: string): number => { + if (checks.has(name)) return 0; + expect(visiting.has(name)).toBe(false); + const known = longest.get(name); + if (known !== undefined) return known; + visiting.add(name); + const depth = 1 + Math.max(0, ...callees.get(name)!.map(run)); + visiting.delete(name); + longest.set(name, depth); + return depth; + }; + for (const f of mod.functions) expect(run(f.name)).toBeLessThanOrEqual(32); + // fn0 breaks both ring cycles, fn33 and fn66 bound the run along the + // ring, and two of a, b and c break their four cycles. + expect([...checks].sort()).toEqual(["a", "b", "fn0", "fn33", "fn66"]); +}); + test("class members are reachable from reentrant operations", () => { const mod: IrModule = { ...moduleWith( diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index 693d20b3b..53b0e56b4 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -37,7 +37,11 @@ export type MayThrowReason = export function computeMayThrow( mod: IrModule, options: { - stackChecks?: boolean; + /** Native stack checks: true for the functions that check on entry (one + * per call cycle, see stackCheckedFunctions); "everyCycleMember" for a + * model in which every function on a cycle checks, which answers a + * superset that does not depend on that choice. */ + stackChecks?: boolean | "everyCycleMember"; explain?: boolean; /** computePublishedTypes(mod), when the caller already has it. */ published?: ReturnType; @@ -56,8 +60,16 @@ export function computeMayThrow( stackChecks?: Set; } { const terminations = mod.workers === true ? workerTerminationPolls(mod, options.nodes) : null; + const stackGraph = + options.stackChecks === undefined || options.stackChecks === false + ? null + : stackCheckGraph(mod, options.nodes); const stackChecks = - options.stackChecks === true ? stackCheckedFunctions(mod, options.nodes) : null; + stackGraph === null + ? null + : options.stackChecks === "everyCycleMember" + ? stackGraph.cyclic + : cycleGuards(stackGraph); // Stores that can reach a published object throw the frozen TypeError. const published = options.published !== undefined ? options.published : computePublishedTypes(mod); @@ -488,7 +500,9 @@ export function computeMayThrow( fns: may, ...(why !== null ? { why } : {}), indirect, - ...(terminations ? { workerEntryPolls: entryPolls(mod, terminations.entry, stackChecks) } : {}), + ...(terminations + ? { workerEntryPolls: entryPolls(mod, terminations.entry, stackGraph?.cyclic ?? null, may) } + : {}), ...(stackChecks ? { stackChecks } : {}), }; } @@ -720,28 +734,32 @@ function describeThrowingNode(rec: IrExpr | IrStmt): string { } /** The termination polls a worker program places on function entry. A - * stack-checked synchronous function needs none: it may throw (the stack - * guard seeds the fixpoint), so every caller tests the pending state after - * the call, and a descent that never returns ends at the stack guard, whose - * RangeError unwinds through those tests. An outstanding terminate request - * keeps the alert word nonzero, so the first test that runs afterwards (a - * post-call test, a loop poll or the unwinding) stops the thread; dropping - * the entry poll only delays the stop by bounded work, which Node's - * asynchronous terminate allows. The entry poll's slow path rejoins the - * body, so every polled function kept its arguments live across a call. */ + * synchronous function on a call cycle that may throw needs none: every + * caller tests the pending state after the call, and a descent that never + * returns passes a stack-checked function of each cycle again and again + * (stackCheckedFunctions) and ends at its guard, whose RangeError unwinds + * through those tests. An outstanding terminate request keeps the alert word + * nonzero, so the first test that runs afterwards (a post-call test, a loop + * poll or the unwinding) stops the thread; dropping the entry poll only + * delays the stop by bounded work, which Node's asynchronous terminate + * allows. The entry poll's slow path rejoins the body, so every polled + * function kept its arguments live across a call. */ function entryPolls( mod: IrModule, entry: Set, - stackChecks: Set | null, + cyclic: Set | null, + may: Set, ): Set { - if (stackChecks === null) return entry; + if (cyclic === null) return entry; // Calling an async or generator function never tests the caller's pending // state, and the module entry has no caller: those keep their polls. const keep = new Set( mod.functions.filter((fn) => fn.async || fn.generator !== undefined).map((fn) => fn.name), ); keep.add(mod.entry); - return new Set([...entry].filter((name) => keep.has(name) || !stackChecks.has(name))); + return new Set( + [...entry].filter((name) => keep.has(name) || !cyclic.has(name) || !may.has(name)), + ); } /** Statement and expression kinds that cannot reenter scriptc code except @@ -971,16 +989,31 @@ export function workerTerminationPolls( return { entry, polls }; } -/** The functions that can take part in unbounded recursion, which check the - * native stack guard on entry: every function on a cycle of the call graph. - * Direct edges are calls, constructions and virtual dispatch. A function - * containing any operation that can reenter scriptc code other than through - * those edges (an indirect call, a callback-taking or dynamic operation, a - * library call; see boundedNode) also reaches every function that such an - * operation can invoke: closure targets and class members. Non-recursive - * functions, including leaf callbacks, stay check-free. */ +/** The call graph that bounds recursion, and the functions on its cycles: + * the ones that can take part in unbounded recursion. Direct edges are + * calls, constructions and virtual dispatch. A function containing any + * operation that can reenter scriptc code other than through those edges + * (an indirect call, a callback-taking or dynamic operation, a library call; + * see boundedNode) also reaches every function that such an operation can + * invoke: closure targets and class members, behind one shared `reentry` + * node. */ +interface StackCheckGraph { + edges: Map; + cyclic: Set; +} + +const REENTRY = "\0reentry"; + +/** The functions that check the native stack guard on entry: one function + * of every cycle of the call graph (stackCheckGraph), which bounds every + * unbounded recursion, since its frames repeat the cycle's guarded function + * again and again. Non-recursive functions, including leaf callbacks, stay + * check-free. See cycleGuards for the choice. */ export function stackCheckedFunctions(mod: IrModule, nodes?: IrNodeIndex): Set { - const reentry = "\0reentry"; + return cycleGuards(stackCheckGraph(mod, nodes)); +} + +function stackCheckGraph(mod: IrModule, nodes?: IrNodeIndex): StackCheckGraph { const methodImpls = new Map(); const memberPrefixes = new Set(); for (const cls of mod.classes ?? []) { @@ -1019,16 +1052,157 @@ export function stackCheckedFunctions(mod: IrModule, nodes?: IrNodeIndex): Set names.has(callee)); - if (reenters) callees.push(reentry); + if (reenters) callees.push(REENTRY); edges.set(fn.name, callees); } edges.set( - reentry, + REENTRY, [...targets].filter((name) => names.has(name)), ); const cyclic = cyclicFunctions(edges); - cyclic.delete(reentry); - return cyclic; + cyclic.delete(REENTRY); + return { edges, cyclic }; +} + +/** The longest run of calls between unchecked functions of a cycle that + * cycleGuards allows; deeper runs get a check. Acyclic call chains below a + * check are as deep as the program makes them already, so this keeps the + * stack used between two checks of the same order. */ +const MAX_UNCHECKED_CYCLE_RUN = 32; + +/** A small set of functions that meets every cycle of the stack-check graph + * (a feedback vertex set), chosen greedily: functions that cannot lie on a + * remaining cycle (no caller or no callee left among the candidates) drop + * out, a function that calls itself is chosen, and otherwise the function + * with the most caller-callee pairs, the one that breaks the most cycles, + * is chosen and removed; ties go to the earlier function. The reentry node + * is not a function and is never chosen. A last pass in topological order + * of the unchosen cycle functions adds a check where a run of them would + * exceed MAX_UNCHECKED_CYCLE_RUN calls. */ +function cycleGuards(graph: StackCheckGraph): Set { + const { edges, cyclic } = graph; + const chosen = new Set(); + if (cyclic.size === 0) return chosen; + const order = new Map(); + const callees = new Map>(); + const callers = new Map>(); + const candidates = [...[...edges.keys()].filter((name) => cyclic.has(name)), REENTRY]; + for (const name of candidates) { + order.set(name, order.size); + callees.set(name, new Set()); + callers.set(name, new Set()); + } + for (const name of candidates) + for (const callee of edges.get(name) ?? []) + if (callees.has(callee)) { + callees.get(name)!.add(callee); + callers.get(callee)!.add(name); + } + // Max-heap of [score, order, name]; an entry is stale once its function + // is gone or its score changed, and is skipped when popped. + const heap: [number, number, string][] = []; + const score = (name: string): number => + name === REENTRY ? -1 : callers.get(name)!.size * callees.get(name)!.size; + const above = (x: [number, number, string], y: [number, number, string]): boolean => + x[0] > y[0] || (x[0] === y[0] && x[1] < y[1]); + const push = (name: string): void => { + heap.push([score(name), order.get(name)!, name]); + for (let i = heap.length - 1; i > 0;) { + const parent = (i - 1) >> 1; + if (!above(heap[i]!, heap[parent]!)) break; + [heap[i], heap[parent]] = [heap[parent]!, heap[i]!]; + i = parent; + } + }; + const pop = (): [number, number, string] => { + const top = heap[0]!; + const last = heap.pop()!; + if (heap.length > 0) { + heap[0] = last; + for (let i = 0; ;) { + let best = i; + for (const child of [2 * i + 1, 2 * i + 2]) + if (child < heap.length && above(heap[child]!, heap[best]!)) best = child; + if (best === i) break; + [heap[i], heap[best]] = [heap[best]!, heap[i]!]; + i = best; + } + } + return top; + }; + const doomed: string[] = []; + const remove = (name: string): void => { + const out = callees.get(name)!; + const into = callers.get(name)!; + callees.delete(name); + callers.delete(name); + for (const callee of out) + if (callee !== name) { + const set = callers.get(callee)!; + set.delete(name); + if (set.size === 0) doomed.push(callee); + else push(callee); + } + for (const caller of into) + if (caller !== name) { + const set = callees.get(caller)!; + set.delete(name); + if (set.size === 0) doomed.push(caller); + else push(caller); + } + }; + // Removes the functions that can no longer lie on a cycle, transitively. + const settle = (): void => { + while (doomed.length > 0) { + const name = doomed.pop()!; + if (callees.has(name)) remove(name); + } + }; + for (const name of candidates) { + if (callees.get(name)!.size === 0 || callers.get(name)!.size === 0) doomed.push(name); + else push(name); + } + settle(); + for (const name of candidates) + if (callees.get(name)?.has(name) === true) { + chosen.add(name); + remove(name); + settle(); + } + while (heap.length > 0) { + const [entryScore, , name] = pop(); + if (!callees.has(name) || entryScore !== score(name) || name === REENTRY) continue; + chosen.add(name); + remove(name); + settle(); + } + // Bound the runs of unchecked cycle functions (acyclic once the chosen + // ones are removed; the reentry node takes no frame): the longest run + // ending at each function, in topological order, and a check where it + // would grow too long. + const unchecked = candidates.filter((name) => !chosen.has(name)); + const pending = new Map(unchecked.map((name) => [name, 0])); + for (const name of unchecked) + for (const callee of new Set(edges.get(name))) + if (pending.has(callee)) pending.set(callee, pending.get(callee)! + 1); + const run = new Map(); + const ready = unchecked.filter((name) => pending.get(name) === 0); + for (let i = 0; i < ready.length; i++) { + const name = ready[i]!; + let depth = (run.get(name) ?? 0) + (name === REENTRY ? 0 : 1); + if (depth > MAX_UNCHECKED_CYCLE_RUN && name !== REENTRY) { + chosen.add(name); + depth = 0; + } + for (const callee of new Set(edges.get(name))) { + if (!pending.has(callee)) continue; + run.set(callee, Math.max(run.get(callee) ?? 0, depth)); + const left = pending.get(callee)! - 1; + pending.set(callee, left); + if (left === 0) ready.push(callee); + } + } + return chosen; } /** Functions on a cycle of the call graph (Tarjan's strongly connected diff --git a/packages/compiler/src/backend/native-codegen-core.ts b/packages/compiler/src/backend/native-codegen-core.ts index dd17466eb..d75dc2097 100644 --- a/packages/compiler/src/backend/native-codegen-core.ts +++ b/packages/compiler/src/backend/native-codegen-core.ts @@ -32,6 +32,25 @@ export function nativeProgramPartitions(llvmBytes: number): number { return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required))); } +/** LLVM text that an optimized one-partition program generates code for on + * one thread, the LLVM text per code generation part, and the most parts. */ +const SINGLE_CODEGEN_BYTES = 128 * 1024; +const CODEGEN_PART_BYTES = 64 * 1024; +const MAX_CODEGEN_PARTS = 8; + +/** Code generation parts for an optimized executable object that + * nativeProgramPartitions keeps as one partition. The helper optimizes the + * module whole, as for a single output, and then generates code for the parts + * concurrently (`--split codegen`), so the generated code does not change. + * Returns 1 for modules of at most SINGLE_CODEGEN_BYTES and for partitioned + * ones. The count is a power of two that depends only on the module's size, + * so artifacts never vary with the host. */ +export function nativeCodegenParts(llvmBytes: number): number { + if (llvmBytes <= SINGLE_CODEGEN_BYTES || llvmBytes > SINGLE_PARTITION_BYTES) return 1; + const required = Math.ceil(llvmBytes / CODEGEN_PART_BYTES); + return Math.min(MAX_CODEGEN_PARTS, 2 ** Math.ceil(Math.log2(required))); +} + /** Output paths for each partition; the first is the requested path. */ export function nativePartitionPaths(path: string, count: number): string[] { const dot = path.lastIndexOf("."); diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index 5463b868e..8ef940c6a 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"; @@ -8,7 +8,11 @@ import { NativeCodegenError, validateNativeCodegenVersion, } from "./native-codegen.js"; -import { nativePartitionPaths, nativeProgramPartitions } from "./native-codegen-core.js"; +import { + nativeCodegenParts, + nativePartitionPaths, + nativeProgramPartitions, +} from "./native-codegen-core.js"; import { nativeArtifactDependenciesStillMatch } from "./native-toolchain.js"; import { LINUX_X64_GNU_TARGET, MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; @@ -26,6 +30,7 @@ async function fakePackage( emptyOutput?: boolean; missingOutput?: boolean; changePackageDuringEmit?: boolean; + profileReport?: string; } = {}, ) { const root = await mkdtemp(join(tmpdir(), "scriptc-native-helper-test-")); @@ -72,6 +77,7 @@ ${ ? ":" : 'for each in $outputs; do cp "$input" "$each"; done' } +${options.profileReport === undefined ? "" : `printf '%s\\n' '${options.profileReport}'`} `, ); await chmod(bin, 0o755); @@ -196,6 +202,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")); @@ -372,6 +413,41 @@ test("program partitions depend only on module size", () => { ]); }); +test("code generation parts depend only on module size", () => { + const kilobyte = 1024; + expect(nativeCodegenParts(128 * kilobyte)).toBe(1); + expect(nativeCodegenParts(128 * kilobyte + 1)).toBe(4); + expect(nativeCodegenParts(256 * kilobyte)).toBe(4); + expect(nativeCodegenParts(256 * kilobyte + 1)).toBe(8); + expect(nativeCodegenParts(1024 * kilobyte)).toBe(8); + // Partitioned modules already compile their partitions concurrently. + expect(nativeCodegenParts(1024 * kilobyte + 1)).toBe(1); +}); + +test("split code generation passes --split codegen and no partition cache", async () => { + const pkg = await fakePackage(); + const output = join(pkg.root, "split", "program.o"); + const artifact = await emitNativeArtifact({ + ...request(pkg.root, pkg.packageJson, output), + partitions: 4, + splitCodegen: true, + }); + expect(artifact.outputPaths).toEqual(nativePartitionPaths(output, 4)); + const args = (await readFile(pkg.log, "utf8")).trim().split(" "); + expect(args.filter((arg) => arg === "--output")).toHaveLength(4); + expect(args.join(" ")).toContain("--split codegen"); + expect(args).not.toContain("--cache-dir"); + + // Partitions of the same module are a distinct artifact. + await emitNativeArtifact({ + ...request(pkg.root, pkg.packageJson, join(pkg.root, "partitions", "program.o")), + partitions: 4, + }); + const calls = (await readFile(pkg.log, "utf8")).trim().split("\n"); + expect(calls).toHaveLength(2); + expect(calls[1]).not.toContain("--split"); +}); + test("partitioned objects are emitted, cached and restored together", async () => { const pkg = await fakePackage(); const first = join(pkg.root, "first", "program.o"); diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index 413fb7e02..ba04bd7d6 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 { @@ -115,6 +125,9 @@ export interface NativeCodegenOptions { cacheRoot?: string | null; /** Program partitions, written to nativePartitionPaths(outputPath). */ partitions?: number; + /** With several partitions: optimize the module whole and divide only its + * code generation among them. */ + splitCodegen?: boolean; /** Profile-guided optimization: instrumentation or an indexed profile. */ profile?: NativeProfileOptions; } @@ -341,6 +354,7 @@ function cacheKey( .update("\0") .update(options.sourcePath) .update((options.partitions ?? 1) > 1 ? `\0partitions:${options.partitions}` : "") + .update(options.splitCodegen === true ? "\0split:codegen" : "") .update(options.profile === undefined ? "" : `\0${nativeProfileKey(options.profile)}`) .digest("hex"); } @@ -436,12 +450,15 @@ 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. const partitionCache = - root === null || outputs.length < 2 + root === null || outputs.length < 2 || options.splitCodegen === true ? null : join( root, @@ -459,6 +476,7 @@ 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); @@ -480,6 +498,7 @@ export async function emitNativeArtifact( "--source-path", options.sourcePath, ...(partitionCache === null ? [] : ["--cache-dir", partitionCache]), + ...(options.splitCodegen === true && outputs.length > 1 ? ["--split", "codegen"] : []), ...nativeProfileHelperArgs(options.profile), ]); for (const stage of stages) { @@ -493,6 +512,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. @@ -500,23 +521,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); - const profileMatch = - options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout); 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 574eceb9d..a1ecc66a3 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -64,6 +64,9 @@ export interface NativeLinkFeatures { eventsOnceSignal?: boolean; /** util.MIMEType and util.MIMEParams: scr_mime.c. */ mime?: boolean; + /** `--large-pages`: scr_text_pages.c, which moves the executable's code + * onto transparent huge pages at startup. Absent (false) without it. */ + largePages?: boolean; /** util.debounce and util.throttle: scr_util_timing.c. */ utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values link diff --git a/packages/compiler/src/backend/native-profile.test.ts b/packages/compiler/src/backend/native-profile.test.ts index b2cb0c75d..18f0c6f73 100644 --- a/packages/compiler/src/backend/native-profile.test.ts +++ b/packages/compiler/src/backend/native-profile.test.ts @@ -3,6 +3,7 @@ import { executableLinkInputs } from "./link-plan-core.js"; import { parseNativeProfileMatch, nativeProfileHelperArgs, + renderNativeProfileMatch, nativeProfileKey, nativeProfileRefusal, profileGenerateLinkerArgs, @@ -84,3 +85,49 @@ test("the helper's profile match is read from its output", () => { 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 904994a7d..85b1fdca4 100644 --- a/packages/compiler/src/backend/native-profile.ts +++ b/packages/compiler/src/backend/native-profile.ts @@ -83,6 +83,32 @@ export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | nu 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 f1bcde404..699847a2a 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, @@ -38,6 +39,8 @@ import { warmNativeCaches, } from "./native-toolchain.js"; import { splitLlvmProgram } from "./llvm/split.js"; +import { implicitToolchainFingerprints } from "./native/compiler-fingerprint.js"; +import { fingerprintDependencyPaths } from "./native/dependency-files.js"; const scratch: string[] = []; const TEST_CACHE_IDENTITY = "cc-cache-tests-v1"; @@ -325,9 +328,9 @@ test.skipIf( let vendorEntries = await readdir(vendorRoot); expect(vendorEntries.filter((name) => name.startsWith("mbedtls-")).length).toBe(2); - // Host clang uses system zlib, so only the Zig build materializes a zlib - // object family in the shared vendor root. - expect(vendorEntries.filter((name) => name.startsWith("zlib-")).length).toBe(1); + // Host clang and Zig each materialize their own vendored zlib object + // family in the shared vendor root. + expect(vendorEntries.filter((name) => name.startsWith("zlib-")).length).toBe(2); // Invalidate the host archive. A targetless Zig rebuild must continue to // use the separately keyed Zig archive instead of repairing or consuming // the host-clang entry. @@ -624,6 +627,76 @@ 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 and for + // (zlib's RVV code) outside RISC-V with the vector + // extension. 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(""); + 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", + "riscv64-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(clangExecutable === undefined)( + "the implicit toolchain probe leaves out a header the compiler rejects", + async () => { + // A system header that the runtime spells, shadowed by one that is an + // #error, as clang 18's is outside RISC-V. The probe + // still covers the other headers. + const dir = await mkdtemp(join(tmpdir(), "scriptc-probe-header-")); + scratch.push(dir); + await mkdir(join(dir, "sys")); + const rejected = join(dir, "sys", "auxv.h"); + await writeFile(rejected, '#error "not for this target"\n'); + const includes = await implicitDependencyProbeIncludes(runtimeSrcDir()); + expect(includes).toContain(""); + const fingerprints = await implicitToolchainFingerprints( + { argv: [clangExecutable!, "-isystem", dir], targetArgs: [], target: null }, + "probe-rejected-header", + ); + const dependencies = fingerprintDependencyPaths(fingerprints.complete); + expect(dependencies).not.toContain(rejected); + expect(dependencies.some((path) => path.endsWith("/stdatomic.h"))).toBe(true); + }, +); + test.skipIf( process.platform === "win32" || clangExecutable === undefined || 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.test.ts b/packages/compiler/src/backend/native-tools.test.ts new file mode 100644 index 000000000..11bb8f3ba --- /dev/null +++ b/packages/compiler/src/backend/native-tools.test.ts @@ -0,0 +1,92 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test, vi } from "vitest"; +import { + NATIVE_CODEGEN_LLVM_VERSION, + NATIVE_CODEGEN_PROTOCOL_VERSION, +} from "./native-codegen-core.js"; +import { verifyNativeHelper } from "./native-tools.js"; +import { LINUX_X64_GNU_TARGET } from "./targets.js"; + +vi.mock("node:child_process", async (original) => ({ + ...(await original()), + execFileSync: vi.fn(), +})); + +const directories: string[] = []; +afterEach(() => { + vi.resetAllMocks(); + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +/** A helper package directory and the options that verify it. */ +function helperOptions() { + const packageRoot = mkdtempSync(join(tmpdir(), "scriptc-helper-")); + directories.push(packageRoot); + const target = LINUX_X64_GNU_TARGET; + const helper = target.helper; + writeFileSync( + join(packageRoot, "package.json"), + JSON.stringify({ name: helper.packageName, version: "1.2.3" }), + ); + vi.mocked(execFileSync).mockReturnValue( + JSON.stringify({ + ok: true, + protocol_version: NATIVE_CODEGEN_PROTOCOL_VERSION, + scriptc_package_version: "1.2.3", + llvm_version: NATIVE_CODEGEN_LLVM_VERSION, + default_target: helper.defaultTarget, + data_layout: helper.defaultDataLayout, + targets: [target.llvmBackend], + supported_targets: [target.llvmTriple], + host_triple: target.llvmTriple, + }), + ); + return { + executable: join(packageRoot, "helper"), + packageRoot, + compilerVersion: "1.2.3", + target, + helper, + }; +} + +test("a helper the verification record knows is not asked for its version again", () => { + const options = helperOptions(); + let known = false; + const remember = vi.fn(() => { + known = true; + }); + const verified = { known: () => known, remember }; + verifyNativeHelper({ ...options, verified }); + expect(execFileSync).toHaveBeenCalledTimes(1); + expect(remember).toHaveBeenCalledTimes(1); + verifyNativeHelper({ ...options, verified }); + expect(execFileSync).toHaveBeenCalledTimes(1); + expect(remember).toHaveBeenCalledTimes(1); +}); + +test("a helper whose version answer is rejected is not recorded", () => { + const options = helperOptions(); + vi.mocked(execFileSync).mockReturnValue(JSON.stringify({ ok: false })); + const remember = vi.fn(); + expect(() => + verifyNativeHelper({ ...options, verified: { known: () => false, remember } }), + ).toThrow(/incompatible/); + expect(remember).not.toHaveBeenCalled(); +}); + +test("the package identity is checked even for a helper the record knows", () => { + const options = helperOptions(); + const remember = vi.fn(); + expect(() => + verifyNativeHelper({ + ...options, + compilerVersion: "9.9.9", + verified: { known: () => true, remember }, + }), + ).toThrow(/package mismatch/); + expect(execFileSync).not.toHaveBeenCalled(); +}); diff --git a/packages/compiler/src/backend/native-tools.ts b/packages/compiler/src/backend/native-tools.ts index 2f1b88ed0..5056de491 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, @@ -44,14 +49,18 @@ export function emitNativeObject(options: { outputKind?: NativeCodegenOutputKind; /** Program partitions, written to nativePartitionPaths(outputPath). */ partitions?: number; + /** With several partitions: optimize the module whole and divide only its + * code generation among them. */ + splitCodegen?: boolean; /** Private directory where the helper reuses partition artifacts. */ partitionCache?: string; /** Profile-guided optimization: instrumentation or an indexed profile. */ profile?: NativeProfileOptions; -}): string[] { + verified?: NativeHelperVerification; +}): { 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, @@ -68,13 +77,18 @@ export function emitNativeObject(options: { "json", "--source-path", options.sourcePath, - ...(options.partitionCache === undefined || outputs.length < 2 - ? [] - : ["--cache-dir", options.partitionCache]), + ...(options.splitCodegen === true && outputs.length > 1 + ? ["--split", "codegen"] + : options.partitionCache === undefined || outputs.length < 2 + ? [] + : ["--cache-dir", options.partitionCache]), ...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. */ @@ -98,13 +112,24 @@ export function mergeNativeProfiles(options: { ]); } -/** Verify installed identity even when a caller can reuse a cached object. */ +/** Helpers that answered `version` as expected: `known` is true when the + * helper file, as it is now, already did, and `remember` records that this + * one just did. */ +export interface NativeHelperVerification { + known(): boolean; + remember(): void; +} + +/** Verify installed identity even when a caller can reuse a cached object. + * With `verified`, the version probe runs only for a helper it does not + * know yet. */ export function verifyNativeHelper(options: { executable: string; packageRoot: string; compilerVersion: string; target: NativeTargetSpec; helper: NativeHelperSpec; + verified?: NativeHelperVerification; }): void { const identity = JSON.parse(readFileSync(options.packageRoot + "/package.json", "utf8")) as { name: string; @@ -118,8 +143,10 @@ export function verifyNativeHelper(options: { `LLVM helper package mismatch: expected ${options.helper.packageName}@${options.compilerVersion}`, ); } + if (options.verified?.known() === true) return; const version = JSON.parse( runNativeTool(options.executable, ["version", "--format=json"]), ) as Record; validateNativeCodegenVersion(version, options.target, options.helper, options.compilerVersion); + options.verified?.remember(); } diff --git a/packages/compiler/src/backend/native/compiler-fingerprint.ts b/packages/compiler/src/backend/native/compiler-fingerprint.ts index ec427ce0a..3df6cb449 100644 --- a/packages/compiler/src/backend/native/compiler-fingerprint.ts +++ b/packages/compiler/src/backend/native/compiler-fingerprint.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { type CcDriver } from "./driver.js"; import { resolvedToolIdentity, resolvedTool } from "./tool-identity.js"; import { execFileAsync } from "./process.js"; @@ -168,7 +168,7 @@ export function effectiveCompilerInvocationFingerprint( export async function implicitDependencyProbeIncludes(rtDir: string): Promise { const vendor = join(rtDir, "..", "vendor"); const quickjsSources = new Set(LRE_SOURCES); - const zlibSources = new Set(ZLIB_SOURCES); + const zlibSources = new Set(ZLIB_SOURCES.map((source) => basename(source))); const fileGroups = await Promise.all([ nativeSourceFiles(rtDir, false), nativeSourceFiles(join(vendor, "ryu"), false), @@ -181,7 +181,7 @@ export async function implicitDependencyProbeIncludes(rtDir: string): Promise name.endsWith(".h") || zlibSources.has(name), ), ]); @@ -195,14 +195,29 @@ 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 . */ +/** clang's x86 intrinsic headers: every <...intrin.h> but the Windows + * (, , ...), and . */ +function x86OnlyHeader(include: string): boolean { + return include === "" || /^<[a-z0-9_]+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. clang ships for every target, and through + * clang 18 including it without the RISC-V vector extension is an #error. */ +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))" + : include === "" + ? " && defined(__riscv_vector)" : ""; return `#if __has_include(${include})${targetCondition}\n#include ${include}\n#endif\n`; } @@ -384,11 +399,8 @@ async function implicitToolchainFingerprintsFresh( ]; const [dependencyResults, linker, assembler, compilerInvocation] = await Promise.all([ Promise.all( - sources.map((source) => - execFileAsync(compiler, [...probeArgs, source], { - cwd: probeDir, - maxBuffer: 16 * 1024 * 1024, - }), + sources.map((source, index) => + probeIncludeDependencies(compiler, probeArgs, probeDir, source, sourceGroups[index]!), ), ), execFileAsync(compiler, [...prefix, "-print-prog-name=ld"], { cwd: probeDir }), @@ -408,14 +420,7 @@ async function implicitToolchainFingerprintsFresh( const toolSpellings = [linker.stdout.trim(), assemblerSpelling].filter( (value, index, all) => value !== "" && all.indexOf(value) === index, ); - const sourceSet = new Set(sources); - const dependencyPaths = [ - ...new Set( - dependencyResults - .flatMap((dependencies) => parseMakeDependencies(dependencies.stdout)) - .filter((path) => !sourceSet.has(path)), - ), - ].sort(); + const dependencyPaths = [...new Set(dependencyResults.flat())].sort(); probe = { compilerIdentity: compilerIdentity ?? `\0${compiler}`, compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir), @@ -498,6 +503,51 @@ async function implicitToolchainFingerprintsFresh( }; } +/** Headers the dependency probe preprocesses at a time when it probes them + * one by one. */ +const SINGLE_HEADER_PROBES = 8; + +/** The files the compiler reads to preprocess `source`, a probe source that + * includes `includes`, apart from the probe's own sources. When the compiler + * rejects `source`, for example for a header it finds that is an #error + * outside its target ( through clang 18 without the RISC-V + * vector extension), each header is preprocessed on its own, and a header + * that the compiler rejects on its own is left out, as an absent one is. + * Throws the compiler's error when it rejects every header. */ +async function probeIncludeDependencies( + compiler: string, + probeArgs: readonly string[], + probeDir: string, + source: string, + includes: readonly string[], +): Promise { + const dependencies = async (path: string): Promise => { + const result = await execFileAsync(compiler, [...probeArgs, path], { + cwd: probeDir, + maxBuffer: 16 * 1024 * 1024, + }); + return parseMakeDependencies(result.stdout).filter((dependency) => dependency !== path); + }; + try { + return await dependencies(source); + } catch (error) { + const found: string[][] = []; + for (let start = 0; start < includes.length; start += SINGLE_HEADER_PROBES) { + const batch = includes.slice(start, start + SINGLE_HEADER_PROBES); + const results = await Promise.all( + batch.map(async (include, offset) => { + const single = `${source.slice(0, -2)}-${start + offset}.c`; + await writeFile(single, implicitDependencyIncludeDirective(include)); + return dependencies(single).catch(() => null); + }), + ); + for (const result of results) if (result !== null) found.push(result); + } + if (found.length === 0) throw error; + return found.flat(); + } +} + const stableImplicitToolchainMemos = new Map>(); export function implicitToolchainFingerprints( driver: Pick, diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 49fd792ba..ee0533ce3 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -76,14 +76,9 @@ export interface CcOptions { fetch?: boolean; /** The program uses zlib (index.ts detects zlib.* libCalls on the IR): * compiles scr_zlib.c — the regex gating precedent, so zlib-free - * binaries keep their exact link line. The default host-clang build links - * the SYSTEM libz (macOS ships it), byte-identical to the historical line; - * every Zig build compiles the vendored zlib with the selected driver instead - * (ensureZlibObjects — zig has no libz in its sysroots). Compressed bytes may - * differ between the - * system and vendored libraries, which is why the corpus only ever - * compares round-trips and fixed-blob inflation, never raw deflate - * output. */ + * binaries keep their exact link line. Every driver compiles the vendored + * zlib, the copy Node bundles (ensureZlibObjects), so compressed bytes match + * Node's. */ zlib?: boolean; /** The program uses node:zlib's Brotli forms (with zlib): compiles * scr_brotli.c and links the vendored brotli archive. */ @@ -134,6 +129,8 @@ export interface CcOptions { eventsOnceSignal?: boolean; /** util.MIMEType and util.MIMEParams: compiles scr_mime.c. */ mime?: boolean; + /** `--large-pages`: compiles scr_text_pages.c. */ + largePages?: boolean; /** util.debounce and util.throttle: compiles scr_util_timing.c. */ utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values: diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 6444b4c3b..22e7cf2cb 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -217,18 +217,13 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // Blob and File (every blob.* call selects dynAsync, whose unit boxes // their reads as dyn promises). unit((u) => u.opts.dynAsync, "scr_blob.c"), - // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system - // libz on the default host-clang build, vendored objects on Zig builds) - // also serves the native fetch's gzip decoder — spread exactly once. + // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the vendored zlib + // objects also serve the native fetch's gzip decoder — spread exactly once. unitArgs((u) => u.opts.zlib - ? isZigDriver(u.driver) - ? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects] - : [u.rt(join(u.rtDir, "scr_zlib.c"))] + ? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects] : u.nativeFetch - ? isZigDriver(u.driver) - ? ["-I", vendorZlibDir(), ...u.zlibObjects] - : [] + ? ["-I", vendorZlibDir(), ...u.zlibObjects] : [], ), // Brotli (scr_brotli.c) plugs into the zlib engine and links the @@ -252,6 +247,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // 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.largePages === true, "scr_text_pages.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 @@ -389,7 +385,7 @@ const RUNTIME_DEFINES: readonly { when: (opts: CcOptions) => boolean | undefined; }[] = [ { define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy }, - // Checked objects carry the dynamic tier's shapes (scr_json.c's SCR_JS_TIER build); only + // scr_json.c's SCR_JS_TIER build exports the dynamic tier's cache primitives; only // programs with dynamic-tier code select it. { define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true }, { define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace }, @@ -674,11 +670,11 @@ export async function compileCInternal( let lreObjects = regex ? lreObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) : []; - // Vendored zlib is the Zig story — default host-clang builds keep the exact - // historical `-lz` system link (see CcOptions.zlib). The native fetch's - // gzip decoder rides the same objects/link. + // Every driver links the vendored zlib (the copy Node bundles), so + // compressed bytes match Node's. The native fetch's gzip decoder rides the + // same objects. let zlibObjects = - ((opts.zlib ?? false) || nativeFetch) && isZigDriver(driver) + (opts.zlib ?? false) || nativeFetch ? zlibObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) : []; const materializeVendorPrerequisites = async ( @@ -827,11 +823,6 @@ export async function compileCInternal( ...(opts.linkInputs ?? []), ...(opts.systemLibraries ?? []).map((name) => `-l${name}`), ...(opts.frameworks ?? []).flatMap((name) => ["-framework", name]), - // GNU ld resolves libraries from left to right and commonly enables - // --as-needed: host-clang libz must follow scr_zlib.c/scr_fetch.c and every - // 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 @@ -1085,7 +1076,6 @@ export async function compileCInternal( ...(targetPlatform(driver) === "win32" ? ["-ladvapi32", "-liphlpapi", "-lws2_32"] : []), ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), - ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, @@ -1098,7 +1088,6 @@ export async function compileCInternal( ...(targetPlatform(driver) === "win32" ? ["-ladvapi32", "-liphlpapi", "-lws2_32"] : []), ...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []), ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), - ...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []), ...unwinderLinkArgs(driver), ...driver.linkArgs, ...executableLinkFlags, @@ -1519,6 +1508,8 @@ export async function compileCInternal( [], programPublicSymbols, `${stem}.program`, + undefined, + { inProcessElf: true }, ); const publishable = entries.filter((entry) => entry.missed); if (publishable.length > 0) { diff --git a/packages/compiler/src/backend/native/library.ts b/packages/compiler/src/backend/native/library.ts index f0577f3ca..3cf4afa53 100644 --- a/packages/compiler/src/backend/native/library.ts +++ b/packages/compiler/src/backend/native/library.ts @@ -78,9 +78,8 @@ import { CacheInputsChangedError } from "./session.js"; * External-symbol contract: undefined references only to the target's C/math * runtime and system APIs. Windows embedders additionally link advapi32, * iphlpapi, and ws2_32; the platform driver supplies its ordinary CRT and - * kernel imports. Zlib rides the VENDORED per-flavor objects here even on - * hosts because the executable lane's system `-lz` cannot ride inside an - * archive. */ + * kernel imports. Zlib rides the vendored per-flavor objects, as in the + * executable lane. */ export async function compileLibArchive(opts: LibArchiveOptions): Promise { clearCcCaches(); @@ -517,6 +516,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 1afd87f7b..2948299b2 100644 --- a/packages/compiler/src/backend/native/program-shards.ts +++ b/packages/compiler/src/backend/native/program-shards.ts @@ -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; diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index 891fa9312..60f1a8960 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_cycle.c", "scr_publish.c", "scr_region.c", + "scr_arena.c", "scr_itable.c", "scr_slots.c", "scr_alloc.c", 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/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 892e9281e..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 = 46 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v46" 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 c2e9dd07f..1ac7e2718 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -91,6 +91,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynForAwait: boolean; eventsOnceSignal: boolean; mime: boolean; + largePages: boolean; utilTiming: boolean; dynBuffer: boolean; builtinValues: boolean; @@ -295,6 +296,7 @@ export function effectiveRuntimeFeatures( dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, mime: features.mime === true, + largePages: features.largePages === true, utilTiming: features.utilTiming === true, dynBuffer: features.dynBuffer === true, builtinValues: features.builtinValues === true, @@ -399,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( @@ -420,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-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 9c5b61f58..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: 46, marker: "scr_runtime_abi_v46" }, + 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: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 812fd9531..a12c137b2 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -21,7 +21,12 @@ import { platformLinkerSupportsPersistentCache, resolvePlatformLinker, } from "./linker.js"; -import { MACOS_ARM64_TARGET, WINDOWS_X64_MSVC_TARGET, type NativeTargetSpec } from "./targets.js"; +import { + LINUX_X64_GNU_TARGET, + MACOS_ARM64_TARGET, + WINDOWS_X64_MSVC_TARGET, + type NativeTargetSpec, +} from "./targets.js"; const VERSION = compilerReleaseVersion(); @@ -129,7 +134,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: "clang", identity: "fixture clang", @@ -242,6 +247,25 @@ describe("runtime pack manifests", () => { expect(guiPlan.driverFlags).toEqual([...defaultPlan.driverFlags, "-Wl,--subsystem,windows"]); }); + test("Linux runtime-pack links are static PIE only when requested", async () => { + const { packagePath, root } = await fixture(LINUX_X64_GNU_TARGET); + const options = { + target: LINUX_X64_GNU_TARGET, + programObject: join(root, "program.o"), + outPath: join(root, "program"), + features: BASE, + ffi: null, + optimization: "release" as const, + resolver: () => packagePath, + }; + const defaultPlan = await createNativeLinkPlan(options); + const staticPlan = await createNativeLinkPlan({ ...options, staticLink: true }); + expect(defaultPlan.driverFlags).not.toContain("-static-pie"); + expect(defaultPlan.staticLink).toBeUndefined(); + expect(staticPlan.driverFlags).toEqual([...defaultPlan.driverFlags, "-static-pie"]); + expect(staticPlan.staticLink).toBe(true); + }); + test("selection chooses the most-specific variant and feature archive", async () => { const { packagePath } = await fixture(); const resolver = () => packagePath; diff --git a/packages/compiler/src/backend/targets.test.ts b/packages/compiler/src/backend/targets.test.ts index 24d2e31a7..4fb275140 100644 --- a/packages/compiler/src/backend/targets.test.ts +++ b/packages/compiler/src/backend/targets.test.ts @@ -13,9 +13,15 @@ import { WASM32_WASI_TARGET, WINDOWS_X64_MSVC_TARGET, executableOptimizationLinkerArgs, + executableStaticLinkerArgs, ffiExtendsNarrowIntegers, nativeCodegenTarget, nativeCodegenTargetRefusal, + selectNativeTarget, + staticExecutableRefusal, + staticLinkFailureNote, + staticLinkMapArgs, + staticLinkRefusal, windowsSubsystemLinkerArgs, } from "./targets.js"; @@ -232,4 +238,96 @@ describe("native code-generation targets", () => { "unknown Windows subsystem", ); }); + + test("links host glibc executables as static PIE only for --static", () => { + expect(executableStaticLinkerArgs(LINUX_X64_GNU_TARGET, false)).toEqual([]); + expect(executableStaticLinkerArgs(LINUX_X64_GNU_TARGET, true)).toEqual(["-static-pie"]); + expect(executableStaticLinkerArgs(LINUX_ARM64_GNU_TARGET, true)).toEqual(["-static-pie"]); + // musl executables are static already. + expect(executableStaticLinkerArgs(LINUX_X64_MUSL_TARGET, true)).toEqual([]); + expect(executableStaticLinkerArgs(MACOS_ARM64_TARGET, false)).toEqual([]); + expect(() => executableStaticLinkerArgs(MACOS_ARM64_TARGET, true)).toThrow( + "only supported for Linux executables", + ); + }); + + test("refuses --static where glibc cannot be linked statically", () => { + const request = { staticLink: true, outputKind: "exe", sanitize: false }; + expect(staticLinkRefusal({ ...request, staticLink: undefined, target: null })).toBeNull(); + expect(staticLinkRefusal({ ...request, target: LINUX_X64_GNU_TARGET })).toBeNull(); + expect(staticLinkRefusal({ ...request, target: LINUX_X64_MUSL_TARGET })).toBeNull(); + expect(staticLinkRefusal({ ...request, outputKind: "obj", target: LINUX_X64_GNU_TARGET })).toBe( + "--static is only supported for executable output", + ); + expect(staticLinkRefusal({ ...request, sanitize: true, target: LINUX_X64_GNU_TARGET })).toMatch( + "cannot be combined with --sanitize", + ); + for (const target of [MACOS_ARM64_TARGET, WINDOWS_X64_MSVC_TARGET, WASM32_WASI_TARGET, null]) + expect(staticLinkRefusal({ ...request, target })).toBe( + "--static is only supported for Linux executables", + ); + // zig links cross-compiled glibc targets, and links glibc dynamically. + const cross = selectNativeTarget("aarch64-linux-gnu", LINUX_X64_GNU_TARGET, "linux")!; + expect(staticLinkRefusal({ ...request, target: cross })).toMatch("-linux-musl"); + const crossMusl = selectNativeTarget("aarch64-linux-musl", LINUX_X64_GNU_TARGET, "linux")!; + expect(staticLinkRefusal({ ...request, target: crossMusl })).toBeNull(); + }); + + test("refuses static glibc executables whose link map keeps the NSS loader", () => { + const libc = "/lib/x86_64-linux-gnu/libc.a"; + // lld lists the input sections it kept. + const lld = (member: string) => + [ + " VMA LMA Size Align Out In Symbol", + ` 11e1a0 11e1a0 640 16 ${libc}(${member}):(.text)`, + ` 11e7e0 11e7e0 38 1 ${libc}(${member}):(.rodata)`, + ].join("\n"); + const lldPlain = [ + " VMA LMA Size Align Out In Symbol", + ` 2c62c0 2c62c0 a10 16 ${libc}(dl-open.o):(.text)`, + ].join("\n"); + // GNU ld lists discarded sections first, and sections relaxation emptied with size 0. + const bfd = (text: boolean) => + [ + "Discarded input sections", + "", + ` .text 0x0000000000000000 0x640 ${libc}(nss_module.o)`, + "", + "Linker script and memory map", + "", + ` .eh_frame 0x00000000000db750 0x0 ${libc}(nss_module.o)`, + " 0x1b8 (size before relaxing)", + ...(text ? [` .text 0x00000000000a81b0 0x640 ${libc}(nsswitch.o)`] : []), + ].join("\n"); + const refusal = + /^--static cannot link this program: its executable includes host, user or group name lookups/; + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, lld("nss_module.o"))).toMatch( + refusal, + ); + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, lld("nsswitch.o"))).toMatch(refusal); + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, bfd(true))).toMatch(refusal); + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, bfd(false))).toBeNull(); + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, lldPlain)).toBeNull(); + // A program's own string never counts. + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, true, "libnss_files.so.2")).toBeNull(); + expect(staticExecutableRefusal(LINUX_X64_GNU_TARGET, false, lld("nss_module.o"))).toBeNull(); + expect(staticExecutableRefusal(LINUX_X64_MUSL_TARGET, true, lld("nss_module.o"))).toBeNull(); + }); + + test("writes a link map only for --static glibc links", () => { + expect(staticLinkMapArgs(LINUX_X64_GNU_TARGET, true, "/stage/link.map")).toEqual([ + "-Xlinker", + "-Map", + "-Xlinker", + "/stage/link.map", + ]); + expect(staticLinkMapArgs(LINUX_X64_GNU_TARGET, false, "/stage/link.map")).toEqual([]); + expect(staticLinkMapArgs(LINUX_X64_MUSL_TARGET, true, "/stage/link.map")).toEqual([]); + }); + + test("notes glibc's static libraries on failed --static links", () => { + expect(staticLinkFailureNote(LINUX_X64_GNU_TARGET, true)).toMatch("libc.a"); + expect(staticLinkFailureNote(LINUX_X64_GNU_TARGET, false)).toBe(""); + expect(staticLinkFailureNote(LINUX_X64_MUSL_TARGET, true)).toBe(""); + }); }); diff --git a/packages/compiler/src/backend/targets.ts b/packages/compiler/src/backend/targets.ts index a6713aee3..e4ce3ab5f 100644 --- a/packages/compiler/src/backend/targets.ts +++ b/packages/compiler/src/backend/targets.ts @@ -91,6 +91,96 @@ export function executableStripLinkerArgs(platform: string, strip: boolean): str } } +/** Why a `--static` request cannot be linked, or null when it can. A glibc + * executable links statically only through the host's clang driver: zig, + * which links cross-compiled glibc targets, links glibc dynamically. */ +export function staticLinkRefusal(options: { + staticLink: boolean | undefined; + outputKind: string; + sanitize: boolean; + target: Pick | null; +}): string | null { + if (options.staticLink !== true) return null; + if (options.outputKind !== "exe") return "--static is only supported for executable output"; + if (options.sanitize) + return "--static cannot be combined with --sanitize; AddressSanitizer requires a dynamically linked executable"; + const target = options.target; + if (target === null || target.platform !== "linux") + return "--static is only supported for Linux executables"; + if (!target.name.endsWith("-musl") && target.defaultLinker === "zig") + return "--static links glibc from the build host and is only supported for the host's own glibc target; use an -linux-musl target to cross-compile a static executable"; + return null; +} + +/** Link the C library and the other system libraries into the executable. + * glibc executables link as static PIE, so the kernel still loads them at a + * random address; musl executables are always linked statically. */ +export function executableStaticLinkerArgs( + target: Pick, + staticLink: boolean, +): string[] { + if (!staticLink) return []; + const refusal = staticLinkRefusal({ staticLink, outputKind: "exe", sanitize: false, target }); + if (refusal !== null) throw new Error(refusal); + return target.name.endsWith("-musl") ? [] : ["-static-pie"]; +} + +/** The link map a `--static` glibc link writes to `path`, which + * staticExecutableRefusal reads. `-Xlinker` keeps the path one argument + * through the driver and into the linker's job. */ +export function staticLinkMapArgs( + target: Pick, + staticLink: boolean, + path: string, +): string[] { + return staticLink && !target.name.endsWith("-musl") ? ["-Xlinker", "-Map", "-Xlinker", path] : []; +} + +/** The libc.a members of glibc's NSS loader: nss_module.o (glibc 2.33 and + * later) and nsswitch.o. */ +const GLIBC_NSS_LOADER_MEMBER = /libc\.a\((?:nss_module|nsswitch)\.o\)/; + +/** Whether a link map line places code from an archive member in the + * output: lld lists the input sections it kept (`… libc.a(m.o):(.text)`), + * GNU ld each input section with its size (` .text 0x… 0x… …libc.a(m.o)`), + * and lists sections that relaxation emptied with size 0. */ +function keepsText(line: string): boolean { + return /\):\(\.text\)\s*$/.test(line) || /^\s*\.text\s+0x[0-9a-f]+\s+0x0*[1-9a-f]/.test(line); +} + +/** Why a linked `--static` executable is refused, or null. A static glibc + * executable answers host, user and group lookups with glibc's built-in + * `files` and `dns` sources, and loads every other module the run host's + * nsswitch.conf names from that host, which glibc supports only for the + * glibc version the executable was linked with. `linkMap` is the link's + * map (staticLinkMapArgs): the executable has the NSS loader when the map + * places the loader's code in it. GNU ld lists the input sections that + * garbage collection discarded before its memory map. */ +export function staticExecutableRefusal( + target: Pick, + staticLink: boolean, + linkMap: string, +): string | null { + if (!staticLink || target.name.endsWith("-musl")) return null; + const memoryMap = linkMap.indexOf("Linker script and memory map"); + const kept = memoryMap === -1 ? linkMap : linkMap.slice(memoryMap); + const loader = kept + .split("\n") + .some((line) => GLIBC_NSS_LOADER_MEMBER.test(line) && keepsText(line)); + if (!loader) return null; + return "--static cannot link this program: its executable includes host, user or group name lookups (which dns.lookup, net and http connections, fetch and os.userInfo use), and a statically linked glibc executable loads the NSS modules for those lookups from the run host, which glibc supports only for the glibc version it was linked with. Build without --static, or use an -linux-musl target"; +} + +/** The note a failed `--static` glibc link adds to the linker's error. */ +export function staticLinkFailureNote( + target: Pick, + staticLink: boolean, +): string { + return staticLink && !target.name.endsWith("-musl") + ? "\n--static links glibc's static libraries (libc.a, libm.a), which the libc6-dev package installs on Debian and Ubuntu and glibc-static on Fedora" + : ""; +} + export interface NativeTargetSpec { /** Stable scriptc-facing identity used in cache keys and diagnostics. */ name: NativeTargetName; diff --git a/packages/compiler/src/backend/vendor-archives.ts b/packages/compiler/src/backend/vendor-archives.ts index ef63aa4e4..363d14b08 100644 --- a/packages/compiler/src/backend/vendor-archives.ts +++ b/packages/compiler/src/backend/vendor-archives.ts @@ -18,6 +18,12 @@ const execFileAsync = promisify(execFile); * runtime pack's executable_section_elimination flags). */ const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"]; +/** A vendored source's object file name: sources in subdirectories + * (zlib's contrib/optimizations/inflate.c) flatten to one directory. */ +function vendorObjectName(source: string): string { + return source.replaceAll("/", "-").replace(/\.c$/, ".o"); +} + import { QJS_COMMIT, MBEDTLS_VERSION, @@ -25,6 +31,7 @@ import { BROTLI_VERSION, LRE_SOURCES, ZLIB_SOURCES, + zlibInputs, } from "./vendor-inputs.js"; export { QJS_COMMIT, @@ -243,11 +250,18 @@ export function createVendorArchives(context: VendorArchiveContext) { return join(runtimeSrcDir(), "..", "vendor", "zlib"); } - /** The vendored zlib TUs behind Zig zlib support: every root *.c - * except the gzFile file-I/O units (gz*.c — nothing in scr_zlib.c - * references the gzFile API, and those TUs alone want unistd/io headers). - * The default host-clang build links the system libz; Zig builds use this - * list so targetless and explicit-target Zig inputs stay on one toolchain. */ + /** The vendored zlib TUs every C-lane build links (zlibInputs picks them + * per target; the gzFile file-I/O units are never compiled, nothing in + * scr_zlib.c references the gzFile API). */ + + /** The triple zlibInputs reads for a driver: its explicit target, else + * the host. */ + function zlibTriple(driver: Pick): string { + if (driver.target !== null) return driver.target; + const cpu = + process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : process.arch; + return `${cpu}-${process.platform === "win32" ? "windows" : process.platform}`; + } function zlibObjectPaths( sanitize: boolean, @@ -260,15 +274,15 @@ export function createVendorArchives(context: VendorArchiveContext) { (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") + `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; const cacheDir = join(cacheRoot, `zlib-${ZLIB_VERSION}-${flavor}`); - return ZLIB_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o"))); + return zlibInputs(zlibTriple(driver)).sources.map((f) => join(cacheDir, vendorObjectName(f))); } /** The zlib objects for one flavor, compiled lazily on the first zlib-using - * Zig build (~1s) and cached like the lre objects — + * C-lane build (~1s) and cached like the lre objects — * /vendor/zlib----/*.o — with the same atomic-rename * publish (parallel first builds race safely; losers discard their work). - * Plain is -Os, asan matches the final link so the sanitized lane - * instruments the codec too. The flavor keys the driver and target exactly + * Plain is -O3, as Node builds zlib; asan matches the final link so the + * sanitized lane instruments the codec too. The flavor keys the driver and target exactly * like the lre flavor: the keying * must never hand a zig-built object set to a clang link off a shared * directory. */ @@ -282,6 +296,7 @@ export function createVendorArchives(context: VendorArchiveContext) { (sanitize ? "asan" : "plain") + (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") + `-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; + const inputs = zlibInputs(zlibTriple(driver)); return ensureVendorObjects({ sanitize, driver, @@ -289,8 +304,9 @@ export function createVendorArchives(context: VendorArchiveContext) { flavor, name: "zlib", vendor: vendorZlibDir(), - sources: ZLIB_SOURCES, + sources: inputs.sources, objects: zlibObjectPaths(sanitize, driver, buildIdentity, cacheRoot), + flags: [...(sanitize ? [] : ["-O3"]), ...inputs.defines.map((define) => `-D${define}`)], }); } @@ -327,7 +343,7 @@ export function createVendorArchives(context: VendorArchiveContext) { "-c", join(vendor, f), "-o", - join(buildDir, f.replace(/\.c$/, ".o")), + join(buildDir, vendorObjectName(f)), ], { cwd: buildDir }, ); diff --git a/packages/compiler/src/backend/vendor-inputs.test.ts b/packages/compiler/src/backend/vendor-inputs.test.ts new file mode 100644 index 000000000..53e9b8c22 --- /dev/null +++ b/packages/compiler/src/backend/vendor-inputs.test.ts @@ -0,0 +1,80 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { expect, test } from "vitest"; +import { ZLIB_SOURCES, zlibInputs } from "./vendor-inputs.js"; + +const repoRoot = fileURLToPath(new URL("../../../..", import.meta.url)); +const zlibDir = join(repoRoot, "packages/runtime/vendor/zlib"); + +const TRIPLES = [ + // The runtime packs' targets. + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-unknown-linux-android26", + "arm64-apple-macosx14.0.0", + "x86_64-apple-macosx14.0.0", + "arm64-apple-ios15.0", + "arm64-apple-ios15.0-simulator", + "x86_64-pc-windows-msvc", + "wasm32-unknown-wasi", + // Zig cross targets and host spellings. + "x86_64-linux-gnu", + "aarch64-linux-musl", + "aarch64-macos", + "x86_64-windows-gnu", + "aarch64-windows-gnu", + "wasm32-wasi", + "x86_64-linux", + "aarch64-darwin", +]; + +test("the compiler's zlib inputs match the runtime packs' for every target", async () => { + const packInputs = (await import( + pathToFileURL(join(repoRoot, "packages/runtime-pack-common/scripts/zlib-inputs.mjs")).href + )) as { zlibInputs(triple: string): { sources: string[]; defines: string[] } }; + for (const triple of TRIPLES) { + expect(zlibInputs(triple), triple).toEqual(packInputs.zlibInputs(triple)); + } +}); + +test("every zlib input exists, and ZLIB_SOURCES lists every source a target compiles", () => { + const listed = new Set(ZLIB_SOURCES); + for (const triple of TRIPLES) { + for (const source of zlibInputs(triple).sources) { + expect(listed.has(source), `${triple}: ${source}`).toBe(true); + expect(existsSync(join(zlibDir, source)), source).toBe(true); + } + } +}); + +test("zlib inputs follow Node's deps/zlib/zlib.gyp", () => { + const x64 = zlibInputs("x86_64-unknown-linux-gnu"); + expect(x64.sources).toContain("adler32_simd.c"); + expect(x64.sources).toContain("contrib/optimizations/inflate.c"); + expect(x64.sources).not.toContain("inflate.c"); + expect(x64.defines).toEqual([ + "HAVE_HIDDEN", + "ADLER32_SIMD_SSSE3", + "X86_NOT_WINDOWS", + "INFLATE_CHUNK_SIMD_SSE2", + "INFLATE_CHUNK_READ_64LE", + "DEFLATE_SLIDE_HASH_SSE2", + ]); + expect(zlibInputs("x86_64-pc-windows-msvc").defines).toContain("X86_WINDOWS"); + const mac = zlibInputs("arm64-apple-macosx14.0.0"); + expect(mac.sources).toContain("crc32_simd.c"); + expect(mac.defines).toContain("ARMV8_OS_MACOS"); + expect(mac.defines).toContain("CRC32_ARMV8_CRC32"); + const ios = zlibInputs("arm64-apple-ios15.0"); + expect(ios.sources).not.toContain("crc32_simd.c"); + expect(ios.defines).not.toContain("CRC32_ARMV8_CRC32"); + expect(ios.defines).toContain("INFLATE_CHUNK_SIMD_NEON"); + expect(zlibInputs("aarch64-unknown-linux-android26").defines).toContain("ARMV8_OS_LINUX"); + const wasm = zlibInputs("wasm32-unknown-wasi"); + expect(wasm.sources).toContain("inflate.c"); + expect(wasm.sources).not.toContain("contrib/optimizations/inflate.c"); + expect(wasm.defines).toEqual(["HAVE_HIDDEN", "CPU_NO_SIMD"]); +}); diff --git a/packages/compiler/src/backend/vendor-inputs.ts b/packages/compiler/src/backend/vendor-inputs.ts index a9d13e2f0..270815b00 100644 --- a/packages/compiler/src/backend/vendor-inputs.ts +++ b/packages/compiler/src/backend/vendor-inputs.ts @@ -1,22 +1,98 @@ /** 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 zlib Node v24.22.0 bundles (`process.versions.zlib` there), the + * Chromium fork in Node's deps/zlib (packages/runtime/vendor/zlib). */ +export const ZLIB_VERSION = "1.3.2.1-motley-285e94b"; /** 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; -export const ZLIB_SOURCES = [ +const ZLIB_COMMON_SOURCES = [ "adler32.c", "compress.c", + "cpu_features.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", - "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c", ] as const; + +/** Every zlib translation unit some target compiles (zlibInputs). */ +export const ZLIB_SOURCES = [ + ...ZLIB_COMMON_SOURCES, + "adler32_simd.c", + "crc32_simd.c", + "inflate.c", + "contrib/optimizations/inffast_chunk.c", + "contrib/optimizations/inflate.c", +] as const; + +/** The zlib translation units and defines for a target, chosen the way + * Node's deps/zlib/zlib.gyp chooses them. The SIMD units carry their own + * target attributes and check the CPU at run time, so every unit builds with + * the same flags, and compressed output does not depend on the choice. + * packages/runtime-pack-common/scripts/zlib-inputs.mjs keeps the same table + * for the runtime packs. `triple` is an LLVM or Zig target triple. */ +export function zlibInputs(triple: string): { sources: string[]; defines: string[] } { + const [cpu = "", ...rest] = triple.split("-"); + const system = rest.join("-"); + const architecture = + cpu === "x86_64" || cpu === "amd64" + ? "x64" + : cpu === "aarch64" || cpu === "arm64" + ? "arm64" + : cpu; + const ios = system.includes("ios"); + const windows = + system.includes("windows") || system.includes("win32") || system.includes("mingw"); + // Android reads the CPU features through getauxval, as Linux does: Node's + // ARMV8_OS_ANDROID path needs the NDK's cpu-features library. + const linux = system.includes("linux") || system.includes("android"); + const mac = !ios && (system.includes("darwin") || system.includes("macos")); + const sources: string[] = [...ZLIB_COMMON_SOURCES]; + const defines = windows ? [] : ["HAVE_HIDDEN"]; + if (architecture === "x64" && !ios) { + sources.push( + "adler32_simd.c", + "contrib/optimizations/inffast_chunk.c", + "contrib/optimizations/inflate.c", + ); + defines.push( + "ADLER32_SIMD_SSSE3", + windows ? "X86_WINDOWS" : "X86_NOT_WINDOWS", + "INFLATE_CHUNK_SIMD_SSE2", + "INFLATE_CHUNK_READ_64LE", + "DEFLATE_SLIDE_HASH_SSE2", + ); + } else if (architecture === "arm64") { + sources.push("contrib/optimizations/inffast_chunk.c", "contrib/optimizations/inflate.c"); + defines.push( + "INFLATE_CHUNK_SIMD_NEON", + "INFLATE_CHUNK_READ_64LE", + "__ARM_NEON__", + "DEFLATE_SLIDE_HASH_NEON", + ); + if (windows) { + defines.push("ARMV8_OS_WINDOWS"); + } else if (linux || mac) { + // Node builds the Adler-32 and CRC-32 units for every arm64 target but + // iOS and Windows. + sources.push("adler32_simd.c", "crc32_simd.c"); + defines.push( + "ADLER32_SIMD_NEON", + "CRC32_ARMV8_CRC32", + linux ? "ARMV8_OS_LINUX" : "ARMV8_OS_MACOS", + ); + } + } else { + sources.push("inflate.c"); + defines.push("CPU_NO_SIMD"); + } + return { sources, defines }; +} diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 58c462e33..fcaaf1d02 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -28,6 +28,7 @@ import { 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, @@ -114,8 +115,10 @@ async function main(args: string[], host: CliHost): Promise { values.ffi !== undefined || values.profile !== undefined || values.strip || + values.static || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || @@ -174,9 +177,11 @@ async function main(args: string[], host: CliHost): Promise { values.ffi !== undefined || values.profile !== undefined || values.strip || + values.static || values.sanitize || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || @@ -266,9 +271,11 @@ async function main(args: string[], host: CliHost): Promise { values["fail-on"] !== undefined || values.optimization !== undefined || values.strip || + values.static || values.ffi !== undefined || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["large-pages"] || values["target-cpu"] !== undefined || values["cpu-dispatch"] !== undefined || values["profile-generate"] !== undefined || @@ -280,7 +287,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/--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", + "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--static/--windows-subsystem/--detached-exit/--large-pages/--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); @@ -323,6 +330,9 @@ 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.static) { + fail(`--static 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}`); } @@ -332,6 +342,9 @@ async function main(args: string[], host: CliHost): Promise { if (command === "coverage" && values["detached-exit"]) { fail(`--detached-exit is a build/run option\n\n${USAGE}`); } + if (command === "coverage" && values["large-pages"]) { + fail(`--large-pages is a build/run option\n\n${USAGE}`); + } if (command === "coverage" && values["profile-generate"] !== undefined) { fail(`--profile-generate is a build/run option\n\n${USAGE}`); } @@ -508,7 +521,9 @@ async function main(args: string[], host: CliHost): Promise { sanitize: values.sanitize, ...(values.optimization === undefined ? {} : { optimization: values.optimization }), strip: values.strip, + staticLink: values.static, detachedExit: values["detached-exit"], + largePages: values["large-pages"], ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), ...(values["profile-generate"] === undefined ? {} @@ -627,8 +642,10 @@ async function main(args: string[], host: CliHost): Promise { ...(backend !== undefined ? { backend } : {}), ...(optimization !== undefined ? { optimization } : {}), ...(values.strip ? { strip: true } : {}), + ...(values.static ? { staticLink: true } : {}), ...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}), ...(values["detached-exit"] ? { detachedExit: true } : {}), + ...(values["large-pages"] ? { largePages: true } : {}), ...targetCpuField(targetCpu), ...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }), // The instrumented program resolves a relative pattern against its own @@ -640,6 +657,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 } : {}), @@ -712,6 +730,15 @@ async function main(args: string[], host: CliHost): Promise { } } 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/output-options.ts b/packages/compiler/src/cli/output-options.ts index fb68eb050..207878692 100644 --- a/packages/compiler/src/cli/output-options.ts +++ b/packages/compiler/src/cli/output-options.ts @@ -9,8 +9,10 @@ export interface OutputOptionValues { sanitize: boolean; optimization?: string; strip?: boolean; + staticLink?: boolean; windowsSubsystem?: string; detachedExit?: boolean; + largePages?: boolean; cpuDispatch?: boolean; profileGenerate?: string; profileUse?: string; @@ -73,9 +75,15 @@ export function resolveOutputOptions( if (values.strip && emit !== "exe") { return { ok: false, message: `--strip is only supported with --emit=exe` }; } + if (values.staticLink && emit !== "exe") { + return { ok: false, message: `--static is only supported with --emit=exe` }; + } if (values.detachedExit && emit !== "exe") { return { ok: false, message: `--detached-exit is only supported with --emit=exe` }; } + if (values.largePages && emit !== "exe") { + return { ok: false, message: `--large-pages is only supported with --emit=exe` }; + } if (values.cpuDispatch && emit !== "exe") { return { ok: false, message: `--cpu-dispatch is only supported with --emit=exe` }; } diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index c957dc629..9f2b62d51 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -38,13 +38,14 @@ 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 - values (SC6101), any Node island (eager-node-islands is - the same until deferred evaluation lands), or a gap - island (a package scriptc should compile but cannot yet) + values (SC6101), dynamic slots (SC6104), any Node + island (eager-node-islands is the same until deferred + evaluation lands), or a gap island (a package scriptc + should compile but cannot yet) --backend code generator (llvm) --optimization native optimization posture (default: release/-O2). dev @@ -55,6 +56,11 @@ Options: counting, cache-line-aligned runtime code on x86-64) --strip remove symbol/debug payload from the linked executable for smaller builds (opt in; --emit=exe only) + --static Linux: link the C library into the executable, which + then starts without the dynamic loader. glibc builds + need its static libraries (libc.a) on the build host + and refuse executables that include host, user or + group name lookups (opt in; --emit=exe only) --windows-subsystem Windows executable subsystem (default: console). gui prevents Windows from opening a console window @@ -63,6 +69,11 @@ Options: a helper process so the parent sees the exit sooner. The helper keeps the executable mapped and its memory charged for up to ~100 ms after exit (opt in; --emit=exe only) + --large-pages Linux: at startup, move the executable's code onto 2 MiB + transparent huge pages when the system allows them, for + large programs. Takes about 3 ms per 10 MiB of code, and + all of the moved code stays resident (opt in; --emit=exe + only) --target-cpu x86-64: compile the program's code for a CPU level (default: baseline, which runs on any x86-64 CPU). The @@ -86,8 +97,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; @@ -102,6 +115,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 @@ -164,8 +181,10 @@ export const CLI_OPTIONS = { backend: { type: "string" }, optimization: { type: "string" }, strip: { type: "boolean", default: false }, + static: { type: "boolean", default: false }, "windows-subsystem": { type: "string" }, "detached-exit": { type: "boolean", default: false }, + "large-pages": { type: "boolean", default: false }, "target-cpu": { type: "string" }, "cpu-dispatch": { type: "boolean" }, "profile-generate": { type: "string" }, @@ -178,6 +197,7 @@ export const CLI_OPTIONS = { 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/compile-types.ts b/packages/compiler/src/compile-types.ts index 576dc9987..6ea532b67 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -31,6 +31,11 @@ export interface CompileBaseOptions { optimization?: NativeOptimization; /** Remove symbol/debug payload from an executable at link time. */ strip?: boolean; + /** Link the C library and the other system libraries into the executable + * (`--static`). glibc executables link as static PIE and need the C + * library's static archives on the build host; musl executables are always + * static. Linux executables only. */ + staticLink?: boolean; /** Windows PE executable subsystem. Console is the default; GUI suppresses * automatic console-window creation. Only valid for Windows executables. */ windowsSubsystem?: WindowsSubsystem; @@ -39,6 +44,10 @@ export interface CompileBaseOptions { * executable mapped and the memory charged for up to ~100 ms after exit. * Executables only; no effect on other platforms. */ detachedExit?: boolean; + /** On Linux, move the executable's code onto 2 MiB transparent huge pages + * at startup when the system allows them. The moved code stays resident. + * Executables only; no effect on other platforms. */ + largePages?: boolean; /** 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. @@ -59,9 +68,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. */ diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index fe5dc171e..6d8a19956 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -26,6 +26,7 @@ * counts (classes.ts programLifetimeClass). * * Verified facts land on the IR as `contracts` (IrContracts). */ +import type { DiagnosticCode } from "../diagnostics/codes.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { IrClassDef, @@ -158,7 +159,7 @@ class ContractChecker { return raw.length > max ? `${raw.slice(0, max - 3)}...` : raw; } - report(code: `SC${number}`, loc: SrcLoc, message: string, hint: string): void { + report(code: DiagnosticCode, loc: SrcLoc, message: string, hint: string): void { const key = `${code}\0${contractLocKey(loc)}\0${message}`; if (this.reported.has(key)) return; this.reported.add(key); @@ -518,8 +519,10 @@ class ContractChecker { for (const fn of this.fnByLoc.get(key) ?? []) targets.push({ fn, note }); if (targets.length === 0) return; // Target-independent: the native stack guard (Linux, macOS) counts, so a - // contract holds on every platform. - const mt = computeMayThrow(this.mod, { stackChecks: true, explain: true }); + // contract holds on every platform. Every function on a call cycle counts + // as checking it, a superset of the functions the backend picks, so the + // answer does not depend on that choice. + const mt = computeMayThrow(this.mod, { stackChecks: "everyCycleMember", explain: true }); const proven: string[] = []; for (const { fn, note } of targets) { if (fn.async === true || fn.generator !== undefined) { @@ -1581,6 +1584,15 @@ class ImmutableContracts { for (let steps = 0; steps < 16; steps++) { if (root.kind === "fieldGet" || root.kind === "recordGet") root = root.obj; else if (root.kind === "arrayGet") root = root.arr; + else if (root.kind === "seqExpr") root = root.result; + // A receiver that is the element when it is present and otherwise + // throws (`xs[i]!.push(v)`) is that element read. + else if ( + root.kind === "ternary" && + root.else_.kind === "libCall" && + root.else_.fn === "error.nodeThrow" + ) + root = root.then; else if ( root.kind === "upcast" || root.kind === "downcast" || diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts index 67446f2d1..7d1a6261f 100644 --- a/packages/compiler/src/contracts/int32-elements.ts +++ b/packages/compiler/src/contracts/int32-elements.ts @@ -40,6 +40,7 @@ * Holes, present undefined, out-of-range reads and `length` changes keep * their JavaScript behavior: the contract constrains the values stored, not * the array's shape. */ +import type { DiagnosticCode } from "../diagnostics/codes.js"; import type { ContractNote, ContractAnnotations } from "../frontend/contract-types.js"; import type { IrClassDef, @@ -61,7 +62,7 @@ export interface ElementContractHost { readonly notes: ContractAnnotations; readonly functions: ReadonlyMap; readonly classes: ReadonlyMap; - report(code: `SC${number}`, loc: SrcLoc, message: string, hint: string): void; + report(code: DiagnosticCode, loc: SrcLoc, message: string, hint: string): void; text(loc: SrcLoc, max?: number): string; where(loc: SrcLoc | null): string; displayName(fnName: string): string; diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 58a2cafa6..b94f43a4f 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,21 @@ 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, + ...(slot.partial === true ? { partial: true } : {}), + dynamicOps: slot.operations ?? 0, + })), + } + : {}), ...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}), ...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}), ...(input.nodeIslands !== undefined @@ -668,6 +703,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 +750,66 @@ 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)")}`, + ); + // The costly slots first: most dynamic operations, then source order. + const ranked = slots + .map((slot, index) => ({ slot, index })) + .sort((a, b) => (b.slot.operations ?? 0) - (a.slot.operations ?? 0) || a.index - b.index) + .map(({ slot }) => slot); + for (const slot of ranked.slice(0, DYNAMIC_SLOTS_SHOWN)) + out.push( + ` ${dynamicSlotLabel(slot)}${slot.partial === true ? " (dynamic pieces)" : ""} ${c(DIM, `${where(slot.loc)}${(slot.operations ?? 0) > 0 ? `, ${slot.operations} dynamic operation${slot.operations === 1 ? "" : "s"}` : ""}`)}`, + ); + 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 +850,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/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/codes.test.ts b/packages/compiler/src/diagnostics/codes.test.ts new file mode 100644 index 000000000..9f6b803b0 --- /dev/null +++ b/packages/compiler/src/diagnostics/codes.test.ts @@ -0,0 +1,89 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { DIAGNOSTIC_CODES, RESERVED_DIAGNOSTIC_CODES, RETIRED_DIAGNOSTIC_CODES } from "./codes.js"; +import { FENCE_CODES, UNSUPPORTED } from "./diagnostic.js"; + +const here = import.meta.dirname; +const packagesDir = join(here, "../../.."); +const codesSource = readFileSync(join(here, "codes.ts"), "utf8"); + +function sourceFiles(dir: string): string[] { + return readdirSync(dir, { recursive: true, withFileTypes: true }) + .filter((e) => e.isFile() && e.name.endsWith(".ts") && !e.name.endsWith(".test.ts")) + .map((e) => join(e.parentPath, e.name)); +} + +// Every non-test source file that can name a code. +const sources = [ + ...sourceFiles(join(packagesDir, "compiler/src")), + ...sourceFiles(join(packagesDir, "cli/src")), +] + .filter((file) => file !== join(here, "codes.ts")) + .map((file) => ({ file: file.slice(packagesDir.length), text: readFileSync(file, "utf8") })); + +const active = new Set(Object.keys(DIAGNOSTIC_CODES)); +const retired = new Set(Object.keys(RETIRED_DIAGNOSTIC_CODES)); +const reserved = new Set(Object.keys(RESERVED_DIAGNOSTIC_CODES)); + +/** The keys of one table in codes.ts, as written (duplicates included). */ +function writtenKeys(table: string): string[] { + const start = codesSource.indexOf(`export const ${table} = {`); + const end = codesSource.indexOf("\n}", start); + return [...codesSource.slice(start, end).matchAll(/^ {2}(SC\d{4}):/gm)].map((m) => m[1]!); +} + +test.each(["DIAGNOSTIC_CODES", "RETIRED_DIAGNOSTIC_CODES", "RESERVED_DIAGNOSTIC_CODES"])( + "%s is written once per code, in numeric order", + (table) => { + const keys = writtenKeys(table); + expect(keys.length).toBeGreaterThan(0); + expect(keys).toEqual([...new Set(keys)].toSorted()); + }, +); + +test("a code is active, retired or reserved, never two of them", () => { + for (const code of retired) expect(active.has(code) || reserved.has(code), code).toBe(false); + for (const code of reserved) expect(active.has(code), code).toBe(false); +}); + +test("every code the sources name has a row in codes.ts", () => { + const missing: string[] = []; + for (const { file, text } of sources) { + for (const [code] of text.matchAll(/\bSC\d{4}\b/g)) { + if (!active.has(code) && !retired.has(code) && !reserved.has(code)) { + missing.push(`${code} (${file})`); + } + } + } + expect(missing, "claim each code in packages/compiler/src/diagnostics/codes.ts").toEqual([]); +}); + +test("no source produces a retired code", () => { + const produced: string[] = []; + for (const { file, text } of sources) { + for (const [, code] of text.matchAll(/["'`](SC\d{4})["'`]/g)) { + if (retired.has(code!)) produced.push(`${code} (${file})`); + } + } + expect(produced).toEqual([]); +}); + +test("every active code is used by some source", () => { + const seen = new Set(sources.flatMap(({ text }) => text.match(/\bSC\d{4}\b/g) ?? [])); + const unused = [...active].filter((code) => !seen.has(code)); + expect(unused, "retire codes nothing produces any more").toEqual([]); +}); + +test("the construct tables only name active codes", () => { + for (const code of [...Object.keys(UNSUPPORTED), ...Object.keys(FENCE_CODES)]) { + expect(active.has(code), code).toBe(true); + } +}); + +test("every band in use is documented in diagnostic.ts", () => { + const header = readFileSync(join(here, "diagnostic.ts"), "utf8"); + for (const band of new Set([...active].map((code) => code[2]))) { + expect(header, `SC${band}xxx`).toContain(`SC${band}xxx`); + } +}); diff --git a/packages/compiler/src/diagnostics/codes.ts b/packages/compiler/src/diagnostics/codes.ts new file mode 100644 index 000000000..b9d255df9 --- /dev/null +++ b/packages/compiler/src/diagnostics/codes.ts @@ -0,0 +1,162 @@ +/* Every SC diagnostic code, in one table. + * + * A code is claimed by adding its row here, in numeric order, before any + * site uses it. Two changes that claim the same code then either edit the + * same line (a merge conflict) or add the same key twice (TypeScript error + * TS1117), and codes.test.ts fails when a source file uses a code that has + * no row, uses a retired code, or when a row is out of order. The band of a + * code (its first digit) says what kind of diagnostic it is; diagnostic.ts + * documents the bands. + * + * Codes are part of scriptc's user-facing output and never change meaning: + * when a construct ships, its code moves to RETIRED_DIAGNOSTIC_CODES instead + * of being reused. RESERVED_DIAGNOSTIC_CODES holds codes that open work + * already uses, so other changes do not take them. */ + +/** The active codes, with a short summary of what each reports. */ +export const DIAGNOSTIC_CODES = { + SC0001: "TypeScript error passed through", + SC0002: "project configuration incompatible with scriptc", + SC0003: "malformed imported JSON module", + SC0004: "TypeScript checker panic", + SC1010: "package imports outside the supported set", + SC1012: "default exports/imports (residual edges)", + SC1013: "namespace imports (residual edges)", + SC1014: "re-exports and export lists", + SC1015: "dynamic import()", + SC1016: "circular imports", + SC1020: "class expressions", + SC1030: "var declarations (residual forms)", + SC1031: "destructuring", + SC1040: "loose equality (== and !=)", + SC1042: "logical operators on mixed operand types", + SC1043: "comparing non-number, non-string values", + SC1045: "increment/decrement in expression position", + SC1050: "labeled break/continue", + SC1052: "for-in loops", + SC1062: "destructuring catch clause bindings", + SC1063: "this use of a catch binding", + SC1070: "async/await", + SC1071: "generators", + SC1080: "'this' outside a class method", + SC1090: "this syntax (generic construct fence)", + SC1100: "operations on 'unknown' values", + SC1101: "converting typed values to 'unknown'", + SC1110: "comptime evaluation failure", + SC1120: "regex feature outside the native engine", + SC2001: "type outside the compilable set", + SC2002: "record shape flow outside the width-copy rules", + SC2003: "union-to-union conversion outside the re-tagging rule", + SC2004: "use of a binding whose declaration did not compile", + SC2005: "generic call signature in a value's type", + SC2006: "index-signature object type outside the supported shape", + SC2007: "overloaded function type as a value", + SC2008: "intersection type with no lowering", + SC2009: "component type outside its container or function slot", + SC2010: "runtime JavaScript execution", + SC2011: "'any'-typed values and their operations", + SC2012: "standard-library form without native lowering", + SC2013: "npm package import or value without native lowering", + SC2020: "standard-library or @types/node surface with no lowering", + SC3001: "LLVM IR tier refusal", + SC3002: "target or capability refusal", + SC3003: "native helper installation or version failure", + SC3004: "native helper code-generation failure", + SC4001: "library profile malformed", + SC4002: "library export unresolved", + SC4003: "library export signature not mappable", + SC4004: "async or generator library export", + SC4005: "library graph reaches the event loop or ambient process state", + SC4007: "generic library export signature", + SC4008: "profile-fenced surface reached by a library", + SC4009: "contract sidecar projection failure", + SC4010: "tabled type declared at several sites", + SC4011: "conditional or mapped type producing a tabled type", + SC4012: "library inbound-bytes trap (runtime)", + SC4013: "library escaped exception trap (runtime)", + SC4014: "library range trap (runtime)", + SC4015: "library type trap (runtime)", + SC4016: "library syntax trap (runtime)", + SC4017: "library out-of-memory trap (runtime)", + SC4018: "library internal invariant trap (runtime)", + SC4019: "library other detected trap (runtime)", + SC4020: "npm package imported by a library is not statically eligible", + SC4021: "integer boundary: literal not representable", + SC4022: "integer boundary: value may not be whole", + SC4023: "integer boundary: value out of range", + SC4024: "host callback reference the profile cannot serve", + SC4025: "host callback invoked before registration (runtime)", + SC4026: "library re-entered from a host callback (runtime)", + SC5001: "FFI manifest malformed", + SC5002: "FFI binding is not an ambient function declaration", + SC5003: "FFI signature does not match its native ABI", + SC5004: "FFI native build failure", + SC6001: "divergence: width copy written or compared through", + SC6002: "divergence: observed object key order", + SC6003: "divergence: localeCompare outside the built-in collation", + SC6004: "divergence: cast to an unrelated class", + SC6101: "performance: likely-hot function with untyped values", + SC6102: "performance: declaration-typed boundary note", + SC6103: "performance: shape inference overlay report", + SC6104: "localized failure: slot held as a checked-dynamic value", + SC7001: "NoThrow contract on an async function or generator", + SC7002: "Int32 contract value not provably int32", + SC7003: "Int32 contract cannot be checked", + SC7004: "Immutable contract value may be referenced elsewhere", + SC7005: "Immutable contract cannot be checked", + SC7006: "Acyclic contract in an unchecked position", + SC7007: "Acyclic type can lie on a reference cycle", + SC7008: "ProgramLifetime class extends another class", + SC8002: "Node islands off for a package or module outside the native subset", + SC8003: "package marked native is outside the native subset", + SC8004: "unknown scriptc.packages or scriptc.nodeIslands configuration", + SC8030: "type patch: no range covers the installed copy", + SC8031: "type patch: selector matches nothing", + SC8032: "type patch: declaration already has the type", + SC8033: "type patch: selector ambiguous or not patchable", + SC8101: "user JavaScript: constructor function", + SC8102: "user JavaScript: prototype mutation", + SC8103: "user JavaScript: shape mutation outside the checker's view", + SC8104: "user JavaScript: sloppy-mode semantics", + SC8105: "user JavaScript: builtin and global patching", + SC9001: "internal compiler error", + SC9002: "internal error trap in generated code", +} as const satisfies Record<`SC${number}`, string>; + +/** Codes whose construct shipped or whose diagnostic was removed. Never + * reuse one: old output and documentation still name it. */ +export const RETIRED_DIAGNOSTIC_CODES = { + SC1011: "exports (shipped)", + SC1021: "arrow functions and closures (shipped)", + SC1022: "nested function declarations (shipped)", + SC1023: "object literals (shipped as records)", + SC1024: "arrays (shipped)", + SC1032: "multiple declarations in one statement (shipped)", + SC1033: "let without an initializer (shipped)", + SC1041: "bitwise operators (shipped)", + SC1051: "do-while loops (shipped)", + SC1060: "switch statements (shipped)", + SC1061: "exceptions (shipped)", + SC1121: "stateful g/y regex tests (shipped)", + SC2030: "npm embedding failure", + SC4006: "library export refusal (removed)", + SC6005: "Date strings outside the native parser's grammar", +} as const satisfies Record<`SC${number}`, string>; + +/** Codes that open work already uses, so no other change claims them. When + * that work lands it moves its codes into DIAGNOSTIC_CODES; when it is + * abandoned, delete them here. */ +export const RESERVED_DIAGNOSTIC_CODES = { + SC8001: "Node islands (ni/* branches)", + SC8005: "Node islands (ni/* branches)", + SC8006: "Node islands (ni/* branches)", + SC8010: "Node islands (ni/* branches)", + SC8011: "Node islands (ni/* branches)", + SC8012: "Node islands (ni/* branches)", + SC8020: "Node islands (ni/* branches)", + SC8021: "Node islands (ni/* branches)", + SC8022: "Node islands (ni/* branches)", + SC8023: "Node islands (ni/* branches)", +} as const satisfies Record<`SC${number}`, string>; + +export type DiagnosticCode = keyof typeof DIAGNOSTIC_CODES; diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 6e6d2895f..256365b36 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -1,4 +1,5 @@ -/* Diagnostic codes live in ONE registry. Message strings are never inlined +/* Diagnostics live in ONE registry, and every code is claimed by its row in + * codes.ts (DIAGNOSTIC_CODES). Message strings are never inlined * at rejection sites — every "not supported yet" the compiler can produce is * enumerable here, which is the seed of the future coverage report * ("N constructs prevented static compilation, grouped by code"). @@ -64,7 +65,13 @@ * 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) + * 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) + * SC7xxx contract types (@scriptc/types): a NoThrow, Int32, Immutable, + * Acyclic or ProgramLifetime contract that does not hold or + * cannot be checked (SC7001–SC7008, contracts/check.ts) * SC8xxx 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 @@ -77,6 +84,7 @@ * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; +import type { DiagnosticCode } from "./codes.js"; /* Internal prioritization buckets. NEVER rendered to users — user-facing * output says only what is and isn't supported (plus hints); scheduling is @@ -84,7 +92,7 @@ import type { SrcLoc } from "../ir/ir.js"; export type Milestone = "M1" | "M2" | "M3" | "M4" | "M5" | "later"; export interface ScrDiagnostic { - code: `SC${number}`; + code: DiagnosticCode; message: string; loc: SrcLoc; milestone?: Milestone; @@ -110,6 +118,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 @@ -583,7 +649,7 @@ export const FENCE_CODES: 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: + entry.partial === true + ? `${dynamicSlotLabel(entry)} keeps its compilable shape with checked-dynamic pieces where its type has no static lowering (${entry.reason}); reads of those pieces into typed code are checked` + : `${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/dynamic-tier.ts b/packages/compiler/src/diagnostics/dynamic-tier.ts index 7970d3f42..ff6a79a57 100644 --- a/packages/compiler/src/diagnostics/dynamic-tier.ts +++ b/packages/compiler/src/diagnostics/dynamic-tier.ts @@ -26,7 +26,14 @@ export type UntypedCause = | { kind: "any"; name: string } | { kind: "json-binding"; name: string } | { kind: "json" } - | { kind: "javascript" }; + | { kind: "javascript" } + | { + kind: "slot"; + name: string; + declaredType: string; + /** The declared type this is a dynamic piece of (push-down). */ + owner?: string; + }; export function causePhrase(cause: UntypedCause): string { switch (cause.kind) { @@ -38,6 +45,10 @@ export function causePhrase(cause: UntypedCause): string { return "untyped `JSON.parse` result"; case "javascript": return "untyped JavaScript file"; + case "slot": + return cause.owner === undefined + ? `\`${cause.name}\` is a dynamic slot (declared \`${cause.declaredType}\`, which does not compile)` + : `\`${cause.name}\` is a dynamic piece of \`${cause.owner}\` (declared \`${cause.declaredType}\`, which does not compile)`; } } @@ -95,5 +106,8 @@ export function renderPerformanceWarnings(diags: readonly ScrDiagnostic[]): stri const blocks = diags.map( (d) => `warning ${d.code}: ${d.message}${d.hint === undefined ? "" : `\n hint: ${d.hint}`}`, ); - return `${blocks.join("\n\n")}\n\nThe program compiled. Adding types to these functions lets scriptc compile them to faster code.`; + const closing = diags.every((d) => d.code === "SC6104") + ? "The program compiled. Declared types that compile keep these slots static." + : "The program compiled. Adding types to these functions lets scriptc compile them to faster code."; + return `${blocks.join("\n\n")}\n\n${closing}`; } diff --git a/packages/compiler/src/diagnostics/envelope.test.ts b/packages/compiler/src/diagnostics/envelope.test.ts index 535fd5512..6639e2a73 100644 --- a/packages/compiler/src/diagnostics/envelope.test.ts +++ b/packages/compiler/src/diagnostics/envelope.test.ts @@ -1,9 +1,10 @@ +import type { DiagnosticCode } from "./codes.js"; import { expect, test } from "vitest"; import type { ScrDiagnostic } from "./diagnostic.js"; import { SourceLocator, buildEnvelope, categoryOf, groupDiagnostics, hintOf } from "./envelope.js"; const diag = ( - code: `SC${number}`, + code: DiagnosticCode, message: string, start: number, extra: Partial = {}, diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index bdb545ff3..144e3f860 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -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,17 @@ 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; + partial?: boolean; + dynamicOps: number; + })[]; /** coverage --explain-dynamic only: the report-only tier assignment. */ tiers?: TiersEnvelope; /** coverage only: the WebAssembly modules the build can see, with their @@ -209,7 +241,7 @@ export function isDivergenceCode(code: string): boolean { } export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { - const code = diag.code; + const code: string = diag.code; if (isDivergenceCode(code)) return "divergence"; if (isPerformanceCode(code)) return "performance"; if (code === "SC0001") return diag.typeWorld === true ? "environment" : "invalid-source"; @@ -227,7 +259,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { * carries one. */ export function hintOf(diag: ScrDiagnostic): string { if (diag.hint !== undefined && diag.hint !== "") return diag.hint; - const code = diag.code; + const code: string = diag.code; if (code === "SC0001") return "fix the TypeScript error; scriptc only compiles programs that typecheck under its type world (strict null checks, the ES2025 library, Node types)"; if (code === "SC1090") diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index b3f02c8a7..1f5625fb8 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -220,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)); @@ -237,11 +237,13 @@ test("early executable keys isolate compile modes, paths, implementation, checke { ...f.options, optimization: "dev" }, { ...f.options, optimization: "speed" }, { ...f.options, detachedExit: true }, + { ...f.options, largePages: 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 2bba26cb2..737a78b98 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -12,6 +12,7 @@ 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"; @@ -53,6 +54,8 @@ export interface EarlyExecutableNativeFeatures { optimization?: "dev" | "speed"; workers?: boolean; youngNursery?: boolean; + /** `--large-pages` links scr_text_pages.c. */ + largePages?: boolean; regex: boolean; copying: boolean; textDecoderLegacy: boolean; @@ -129,6 +132,8 @@ interface EarlyExecutableCacheStamp { native: EarlyExecutableNativeFeatures; /** The build's performance warnings (SC61xx), which a cache hit repeats. */ performance?: ScrDiagnostic[]; + /** How a --profile-use build's profile matched, which a cache hit reports. */ + profileMatch?: NativeProfileMatch; integrity: string; } @@ -143,10 +148,14 @@ export interface EarlyExecutableCacheOptions { optimization?: "dev" | "speed"; /** Omitted retains the unstripped executable's historical cache key. */ strip?: true; + /** Omitted unless --static is set. */ + staticLink?: true; /** Omitted for the default Windows console subsystem. */ windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; + /** Omitted unless --large-pages is set. */ + largePages?: true; /** Omitted for the x86-64 baseline (`--target-cpu`). */ targetCpu?: TargetCpu; /** Omitted for builds without runtime CPU dispatch. */ @@ -154,12 +163,19 @@ export interface EarlyExecutableCacheOptions { /** 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; + /** Omitted unless SCRIPTC_HEAP_CENSUS is set (backend/llvm/heap-census.ts). */ + heapCensus?: 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; @@ -213,12 +229,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", @@ -292,13 +319,17 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { options.backend, ...optimizationKeyParts(options.optimization), ...(options.strip ? ["strip"] : []), + ...(options.staticLink ? ["static-link"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), + ...(options.largePages ? ["large-pages"] : []), ...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.heapCensus ? ["heap-census"] : []), options.npmStatic === null ? "" : options.npmStatic === "auto" @@ -336,13 +367,17 @@ 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 }), @@ -861,6 +897,7 @@ export async function publishEarlyExecutableCache( ...(result.performance === undefined || result.performance.length === 0 ? {} : { performance: result.performance }), + ...(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/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 848306b66..d2e881473 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -132,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/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 4ded2a1a0..71bbeb376 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -7,10 +7,12 @@ * variable and class field types, and the class markers (Acyclic, * ProgramLifetime) in heritage clauses. The IR-side checks live in * ir/contract-check.ts. */ +import type { DiagnosticCode } from "../diagnostics/codes.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { SrcLoc } from "../ir/ir.js"; import { locOf } from "./program.js"; import * as ts from "./ts7/adapter.js"; +import { type HeritageElement, isHeritageElement } from "./heritage-elements.js"; export const CONTRACT_TYPES_MODULE = "@scriptc/types"; @@ -86,11 +88,11 @@ function markerOf(checker: ts.TypeChecker, ref: ts.TypeReferenceNode): ContractM } /** The marker a heritage clause names (`implements Acyclic`, - * `implements sc.ProgramLifetime`). */ -function markerOfHeritage( - checker: ts.TypeChecker, - node: ts.ExpressionWithTypeArguments, -): ContractMarker | null { + * `implements sc.ProgramLifetime`), as an ExpressionWithTypeArguments or, + * in TypeScript 7.1's interface `extends` and class `implements`, a + * TypeReference. */ +function markerOfHeritage(checker: ts.TypeChecker, node: HeritageElement): ContractMarker | null { + if (ts.isTypeReferenceNode(node)) return markerOf(checker, node); const e = node.expression; if (ts.isIdentifier(e)) return markerNamed(checker, e, e.text, false); if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && ts.isIdentifier(e.name)) @@ -134,7 +136,7 @@ function nameText(node: ts.Node | undefined): string { } function contractDiag( - code: `SC${number}`, + code: DiagnosticCode, loc: SrcLoc, message: string, hint: string, @@ -168,7 +170,7 @@ export function collectContractAnnotations( ts.walkPreorder( sf, (node) => { - if (ts.isExpressionWithTypeArguments(node)) { + if (isHeritageElement(node)) { const marker = markerOfHeritage(checker, node); if (marker === null) return; out.count++; @@ -231,7 +233,7 @@ function unsupported(out: ContractAnnotations, node: ts.Node, marker: ContractMa function placeHeritage( out: ContractAnnotations, checker: ts.TypeChecker, - node: ts.ExpressionWithTypeArguments, + node: HeritageElement, marker: ContractMarker, resolveUnit: ContractUnitResolver, ): void { 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 ffe01ee47..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"); @@ -73,12 +76,15 @@ function discover(files: Record): { targets: string[]; queried: 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()], { - noLib: true, - types: [], - module: ModuleKind.ESNext, - moduleResolution: ModuleResolutionKind.Bundler, - }); + 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) => diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 93fc7c244..398e1b0aa 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -3,7 +3,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import * as ts from "./ts7/adapter.js"; import { ambientDtsPath, - fallbackDtsPath, + isFallbackDtsPath, isNodeTypesPath, overridesDtsPath, tsgoPath, @@ -22,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)) ); }) ); @@ -333,7 +333,7 @@ const SPELLED_CHILD_PROCESS = new RegExp( function namesChildProcess(program: ts.Program): boolean { const known = program.analysis.namesChildProcess; if (known !== null) return known; - const own = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + 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(); @@ -343,7 +343,7 @@ function namesChildProcess(program: ts.Program): boolean { let names = false; for (const fileName of program.getSourceFileNames()) { const path = tsgoPath(fileName); - if (isNodeTypesPath(fileName) || own.has(path)) continue; + if (isNodeTypesPath(fileName) || isFallbackDtsPath(fileName) || own.has(path)) continue; const text = texts.get(path) ?? program.getSourceText(fileName) ?? ""; if ( text.includes("child_process") || diff --git a/packages/compiler/src/frontend/heritage-elements.test.ts b/packages/compiler/src/frontend/heritage-elements.test.ts new file mode 100644 index 000000000..4ceb3c983 --- /dev/null +++ b/packages/compiler/src/frontend/heritage-elements.test.ts @@ -0,0 +1,82 @@ +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; +import { expect, test } from "vitest"; +import { collectContractAnnotations } from "./contract-types.js"; +import { isHeritageElement, heritageElementTarget } from "./heritage-elements.js"; +import * as ts from "./ts7/adapter.js"; +import { Ts7Api } from "./ts7/api.js"; +import { Ts7Host } from "./ts7/program-host.js"; +import { Ts7RpcClient } from "./ts7/rpc-client.js"; +import { TS7_FILE_SYSTEM_CALLBACKS } from "./ts7/rpc-filesystem.js"; +import { spawnTs7Wire } from "./ts7/rpc-process.js"; +import { ts7TestServers, type Ts7TestServer } from "./ts7/test-servers.js"; + +const corpus = fileURLToPath(new URL("../../../../tests/corpus/", import.meta.url)); + +/** Corpus programs whose classes implement contract markers and interfaces. */ +const programs = [ + "4636-contract-program-lifetime-references.ts", + "5010-lifetime-region-generations.ts", + "7401-program-lifetime-array-teardown.ts", +]; + +function host(server: Ts7TestServer): Ts7Host { + return new Ts7Host( + (options) => + new Ts7Api( + new Ts7RpcClient( + spawnTs7Wire(server.executable, [ + "--api", + "--cwd", + options.cwd, + `--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`, + ]), + ), + options.fs, + options.collectTiming, + ), + { cwd: dirname(corpus) }, + ); +} + +/** Each heritage element's clause token, kind and the text it refers to, + * and the program's contract annotations. */ +function facts(server: Ts7TestServer, file: string) { + const programHost = host(server); + try { + const program = programHost.createProgram([corpus + file], { + strict: true, + types: [], + target: ts.ScriptTarget.ESNext, + }); + const source = program.getSourceFile(corpus + file)!; + const elements: string[] = []; + ts.walkPreorder(source, (node) => { + if (!isHeritageElement(node)) return undefined; + const clause = node.parent as ts.HeritageClause; + elements.push( + `${clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends"} ${ts.getTextOfNode(heritageElementTarget(node))}`, + ); + return undefined; + }); + const contracts = collectContractAnnotations(program.getTypeChecker(), [source]); + return { + elements, + classes: [...contracts.classes.values()].map((note) => `${note.marker} ${note.name}`), + acyclic: contracts.acyclic.map((entry) => entry.note.name), + diagnostics: contracts.diagnostics.map((diagnostic) => diagnostic.code), + }; + } finally { + programHost.close(); + } +} + +test("heritage elements and the contracts they name read the same from tsc 7.0 and tsc 7.1", () => { + const [old, next] = ts7TestServers(); + for (const file of programs) { + const expected = facts(old!, file); + expect(expected.classes.length, file).toBeGreaterThan(0); + expect(expected.diagnostics, file).toEqual([]); + expect(facts(next!, file), file).toEqual(expected); + } +}); diff --git a/packages/compiler/src/frontend/heritage-elements.ts b/packages/compiler/src/frontend/heritage-elements.ts new file mode 100644 index 000000000..b937c073a --- /dev/null +++ b/packages/compiler/src/frontend/heritage-elements.ts @@ -0,0 +1,28 @@ +import * as ts from "./ts7/adapter.js"; + +/** An element of a heritage clause. TypeScript 7.0 parses every element as + * an ExpressionWithTypeArguments. TypeScript 7.1 parses the elements of an + * interface's `extends` and a class's `implements` as TypeReference nodes, + * whose names are entity names (an Identifier or a QualifiedName); a class's + * `extends` element stays an ExpressionWithTypeArguments. */ +export type HeritageElement = ts.ExpressionWithTypeArguments | ts.TypeReferenceNode; + +/** Whether `node` is a TypeReference that is a heritage clause element. */ +export function isHeritageTypeReference(node: ts.Node): node is ts.TypeReferenceNode { + return ( + ts.isTypeReferenceNode(node) && node.parent !== undefined && ts.isHeritageClause(node.parent) + ); +} + +/** Whether `node` is a heritage clause element in either form. */ +export function isHeritageElement(node: ts.Node): node is HeritageElement { + return ts.isExpressionWithTypeArguments(node) || isHeritageTypeReference(node); +} + +/** The name or expression a heritage clause element refers to: its + * expression (an ExpressionWithTypeArguments) or its type name (a + * TypeReference). */ +export function heritageElementTarget(element: ts.Node): ts.Expression | ts.EntityName { + if (ts.isTypeReferenceNode(element)) return element.typeName; + return (element as ts.ExpressionWithTypeArguments).expression; +} diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts index d6f9a91b7..754a59c2f 100644 --- a/packages/compiler/src/frontend/input-tracker.test.ts +++ b/packages/compiler/src/frontend/input-tracker.test.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import * as fs from "node:fs"; import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -301,6 +302,36 @@ test("a settled file is checked by its identity, and any write invalidates it", expect(frontendInputsStillMatch(snapshot)).toBe(false); }); +test("a settled file an earlier run read keeps that run's digest until it is written", 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 settledRead = (tracker: FrontendInputTracker, later: number) => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.now() + later); + try { + tracker.runSynchronous(() => trackedReadFile(source)); + } finally { + vi.useRealTimers(); + } + const probe = tracker.snapshot().probes.find((entry) => entry.op === "file"); + return probe?.op === "file" ? probe : undefined; + }; + const first = settledRead(new FrontendInputTracker(), 5000)!; + expect(first.identity).toBeDefined(); + // A digest only the previous snapshot holds shows it was not computed again. + const marker = "f".repeat(64); + const previous = { version: 1 as const, stable: true, probes: [{ ...first, digest: marker }] }; + expect(settledRead(new FrontendInputTracker(previous), 5000)?.digest).toBe(marker); + await writeFile(source, "export const answer = 2;\n"); + const rewritten = settledRead(new FrontendInputTracker(previous), 10000); + expect(rewritten?.identity).not.toBe(first.identity); + expect(rewritten?.digest).toBe( + createHash("sha256").update("export const answer = 2;\n").digest("hex"), + ); +}); + 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); @@ -316,3 +347,41 @@ test("a file changed shortly before the read is hashed on every check", async () 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 c23a87313..f91e926c4 100644 --- a/packages/compiler/src/frontend/input-tracker.ts +++ b/packages/compiler/src/frontend/input-tracker.ts @@ -84,6 +84,22 @@ function readWithIdentity(path: string): { text: string; identity: string | null 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); @@ -109,6 +125,23 @@ let synchronousTracker: FrontendInputTracker | undefined; export class FrontendInputTracker { private readonly probes = new Map(); private stable = true; + /** The digests `previous` recorded for files read with a settled identity. */ + private readonly known = new Map(); + + /** `previous` is the snapshot of an earlier run whose cache entry is being + * rebuilt: a file whose settled identity has not changed since then has the + * content it hashed, so its digest is not computed again. */ + constructor(previous?: FrontendInputSnapshot) { + for (const probe of previous?.probes ?? []) + if (probe.op === "file" && probe.identity !== undefined) + this.known.set(probe.path, { identity: probe.identity, digest: probe.digest }); + } + + /** The digest an earlier run recorded for `path` with this identity. */ + knownDigest(path: string, identity: string): string | undefined { + const known = this.known.get(path); + return known?.identity === identity ? known.digest : undefined; + } run(fn: () => T): T { const parent = activeTracker.getStore(); @@ -142,12 +175,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 @@ -231,7 +269,11 @@ export function trackedReadFile(path: string): string | null { if (virtual !== undefined) return virtual; try { const { text, identity } = readWithIdentity(path); - const digest = frontendSourceDigest(text); + const known = + identity === null + ? undefined + : (synchronousTracker ?? activeTracker.getStore())?.knownDigest(path, identity); + const digest = known ?? frontendSourceDigest(text); record( identity === null ? { op: "file", path, digest } : { op: "file", path, digest, identity }, ); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index ed52b8da5..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, @@ -446,6 +448,167 @@ export function appendElement( ]; } +/** 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, @@ -529,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) { 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..1922be0af --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/array-classes.ts @@ -0,0 +1,815 @@ +/* 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 { keyedHelperName } from "../stable-names.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 === "union") return arrayClassUnionInstanceOf(lowerer, value, info, 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; +} + +/** `x instanceof X` for a class X declared `extends Array` on a union (an + * element read of an array of X instances is `X | undefined`): an array arm + * tests the array's class at run time, a checked-dynamic arm asks the + * runtime, and every other arm is no instance. */ +function arrayClassUnionInstanceOf( + lowerer: Lowerer, + value: IrExpr, + info: ClassInfo, + loc: SrcLoc, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (arms === undefined) return null; + const className = info.def.name; + const key = `instanceof.arrayClass:${unionId}:${className}`; + let helper = lowerer.valueHelpers.get(key); + if (!helper) { + helper = keyedHelperName(lowerer, "%instanceof.arrayClass", key); + lowerer.valueHelpers.set(key, helper); + const param: IrExpr = { kind: "varRef", localId: "value.0", type: value.type, loc }; + const body: IrStmt[] = []; + arms.forEach((arm, tag) => { + if (arm.kind !== "array" && arm.kind !== "dyn") return; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: param, type: arm, loc }; + const test: IrExpr = + arm.kind === "array" + ? { kind: "instanceOf", value: narrowed, className, type: BOOL, loc } + : { + kind: "libCall", + fn: "array.dynIsClass", + args: [narrowed, { kind: "strLit", value: className, type: STRING, loc }], + type: BOOL, + loc, + }; + body.push({ + kind: "if", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: param, type: BOOL, loc }, + then: [{ kind: "return", value: test, loc }], + else_: null, + loc, + }); + }); + body.push({ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }); + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: value.type }], + returnType: BOOL, + locals: [{ id: "value.0", name: "value", type: value.type, mutable: false }], + body, + loc, + }); + } + return { kind: "call", callee: helper, args: [value], type: BOOL, loc }; +} + +/** 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/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.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/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/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index c7fb8d36e..91eced9c2 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -1,3 +1,5 @@ +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; +import { layoutReturnType } from "./structural-instances.js"; import { typedArgumentsCandidate, typedArgumentsShapes, @@ -181,6 +183,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); } @@ -890,6 +898,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.badType(nameBlame, lowerer.typeOf(nameBlame)); } + if (!isAsync && !isGenerator) returnType = layoutReturnType(lowerer, decl, returnType); const symbol = declSymbolOf(lowerer, decl); if (!symbol) lowerer.unsupported("SC1090", decl, "this function form"); lowerer.fnSigsBySymbol.set(symbol, { diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index bbdf271c0..3e7b7783e 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -41,6 +41,7 @@ function context() { protocolDispatches: new Map(), classMethodValueSelections: new Map(), isSubclassOf: () => false, + nodeAtLoc: () => null, } as unknown as Lowerer; const boxed = fn("box", [ statement({ diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 1c6df3d5c..c19763e8d 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -38,6 +38,7 @@ import { import { streamTypedRefEligible } from "../../ir/analysis.js"; import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js"; import { PoisonError, type Lowerer } from "./lowerer.js"; +import { isSloppyAt } from "./frozen-records.js"; import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js"; import { type ParamShape } from "./call-signatures.js"; import { @@ -2418,6 +2419,11 @@ export class ClassDynamicDispatch { computedRead.key.operand.type.kind === "f64" ? computedRead.key : null; + // A sloppy-mode write ignores a refused assignment + // (sloppyDynamicWrites), so its sites share their own helper, + // whose write takes the first such site's source range. + const sloppyWrite = + !!computedWrite && !reflect && isSloppyAt(lowerer, computedWrite.loc); const key = JSON.stringify([ !!computedWrite, computedRead?.optional ?? false, @@ -2426,6 +2432,7 @@ export class ClassDynamicDispatch { numericKey, reflect, !!indexKey, + ...(sloppyWrite ? ["sloppy"] : []), ]); let dispatch = this.computed.get(key); if (!dispatch) { diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index bff419d48..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 @@ -131,21 +139,19 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width // 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. + // 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 }) => { - const target = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined; - return ( - source !== undefined && - target !== undefined && - !source.tuple && - !target.tuple && - target.fields.length === source.fields.length && - source.fields.every((field) => target.fields.some((kept) => kept.name === field.name)) - ); - }); - if (lossless.length === 1) selected = lossless; + 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 }; @@ -241,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/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts index 1f4013d5e..a8ae99ec1 100644 --- a/packages/compiler/src/frontend/lowering/completion-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -11,7 +11,7 @@ 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 { moduleRuntimeFeatures } from "../../ir/runtime-features.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 @@ -45,6 +45,7 @@ const COMPLETION_CALLBACKS: ReadonlyMap = new Map< ["http.requestDynCb", "last"], ["http.requestUrlCb", "last"], ["net.connectCb", 2], + ["net.connectPathCb", 2], ["net.listenCb", 2], ["net.listenOptsCb", 4], ["net.listenOptsReusePortCb", 5], @@ -148,7 +149,7 @@ function bindCall(e: IrExpr): IrExpr { /** Binds the completion callbacks of `mod` when its scheduling carries * AsyncLocalStorage contexts. */ export function bindCompletionCallbacks(mod: IrModule): void { - if (!moduleRuntimeFeatures(mod).asyncContext) return; + 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 2c4d5d9b4..5eee71231 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -38,6 +38,9 @@ import { } from "../lower-array-sort.js"; import { arrayIndexPresent, + arraySlotSnapshot, + arraySnapshotAppend, + arraySnapshotValue, arrayValueRead, arrayValueStore, arrayValueType, @@ -45,6 +48,9 @@ import { elementMayBeForeign, foreignElementThrow, primitiveElement, + slotSnapshotLocals, + unreadArgument, + type SlotSnapshot, } from "../array-values.js"; import { typeKey } from "../../type-mapper.js"; import { @@ -101,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; @@ -133,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( @@ -148,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 = { @@ -541,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 @@ -562,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, @@ -580,6 +610,7 @@ function arrayHofHelper( outElem, fastT, present, + unread, ); lowerer.liftedFns.push(fastT ? withFastCallback(fn, fastT, elem) : fn); return name; @@ -794,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); @@ -810,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), @@ -858,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), @@ -994,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", @@ -1006,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, }, @@ -1048,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, }, @@ -1088,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 @@ -1109,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" @@ -1134,6 +1225,7 @@ export function lowerArrayFindLikeCall( callbackSite(fast ?? fnArg), fastT, present, + unread, ) : someEveryHelper( lowerer, @@ -1145,6 +1237,7 @@ export function lowerArrayFindLikeCall( callbackSite(fast ?? fnArg), fastT, present, + unread, ); return { kind: "call", @@ -1182,6 +1275,7 @@ export function lowerArrayFindLikeCall( retag, callbackSite(fnArg), present, + unread, ); return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc }; } @@ -1275,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); @@ -1296,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) @@ -1334,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, @@ -1375,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); @@ -1391,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); @@ -1411,6 +1545,7 @@ function findIndexHelper( elem, present, loc, + unread, ), loc, ), @@ -1460,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); @@ -1474,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); @@ -1490,6 +1627,7 @@ function someEveryHelper( elem, present, loc, + unread, ), loc, ), @@ -1553,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" || @@ -1603,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", @@ -1620,6 +1763,7 @@ export function lowerArrayFlatMapCall( callbackSite(fast ?? fnArg), fast?.type ?? null, present, + unread, ); return { kind: "call", @@ -1629,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, @@ -1644,6 +1789,7 @@ export function lowerArrayFlatMapCall( callbackSite(fast ?? fnArg), fast?.type ?? null, present, + unread, ); return { kind: "call", @@ -1943,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); @@ -1957,6 +2104,7 @@ function flatMapHelper( outElem: fnRet.elem, present, split: !!fastT, + unread, }); const arrT = arrayOf(elem); const inner = fnRet.elem; @@ -2053,6 +2201,7 @@ function flatMapHelper( elem, present, loc, + unread, ), loc, ), @@ -2125,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)], @@ -2133,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, @@ -2155,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)); @@ -2185,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); @@ -2201,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); @@ -2210,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 }, @@ -2320,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, ), @@ -3215,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-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 702837e40..cadee368a 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -560,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)); @@ -932,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; diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 6307d871f..5cdbe4d31 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -6,6 +6,7 @@ import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, + DYN, F64, VOID, type IrBindingSource, @@ -32,9 +33,14 @@ import { import { arrayIndexPresent, arrayValueRead, + arraySlotSnapshot, + arraySnapshotAppend, + arraySnapshotValue, arrayValueStore, arrayValueType, elementMayBeForeign, + unreadArgument, + type SlotSnapshot, } from "../array-values.js"; import type { Lowerer } from "../lowerer.js"; @@ -62,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" }; @@ -610,7 +619,13 @@ function chainCallbacks(chain: Chain): Stage[] | null { /** 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; @@ -1595,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]!; @@ -1630,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) { @@ -1690,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; @@ -1791,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(); @@ -1816,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); diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index de027e08c..3b504861a 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -53,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)`, @@ -72,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 @@ -86,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) => ...)`): @@ -276,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/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 87169539f..4b13ba363 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -177,6 +177,8 @@ export function scanDivergences( if (use === null || use === undefined || param === undefined || param.type === undefined) return; if (!isExistingObjectExpression(arg)) return; + // The call kept the argument's layout: the callee has the value itself. + if (lowerer.layoutKeptNodes.has(arg)) return; const copy = safely(() => widthCopyBetween(lowerer, arg, param.type!)); if (copy === undefined || copy === null) return; out.push( @@ -185,6 +187,7 @@ export function scanDivergences( }); } for (const decl of typedDecls) { + if (lowerer.layoutKeptNodes.has(decl.initializer!)) continue; const name = (decl.name as ts.Identifier).text; const scope = enclosingScope(decl); if (scope === undefined) continue; diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts index 3e0cea20a..03994970c 100644 --- a/packages/compiler/src/frontend/lowering/dynamic-tier.ts +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -30,7 +30,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import * as ts from "../ts7/adapter.js"; import { isCheckedJsSourceFile, isJsSourceFile } from "../program.js"; import { isNodeModulesPath } from "../resolve.js"; -import type { Lowerer } from "./lowerer.js"; +import type { DynamicSlot, Lowerer } from "./lowerer.js"; export interface HotDynamicFunction { /** The IR function name (`%Point.norm`, `parse`, ...). */ @@ -208,9 +208,41 @@ function identifierCause(lowerer: Lowerer, id: ts.Identifier, depth = 0): Untype return null; } +/** A dynamic slot (localized failure): the node's declared type degraded + * to the checked dynamic, wholly or in pieces. Null while nothing degraded. */ +function slotCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null { + if (lowerer.degradedTypes.size === 0) return null; + if (!ts.isIdentifier(n) && !ts.isPropertyAccessExpression(n)) return null; + // The declared type of the binding or property, not its narrowed flow + // type (narrowing makes new checker types). + let symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(n) ? n : n.name); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + const type = + symbol !== undefined && (symbol.flags & VALUE_BINDING) !== 0 + ? lowerer.checker.getTypeOfSymbol(symbol) + : lowerer.checker.getTypeAtLocation(n); + const id = (type as { id?: number }).id ?? -1; + // A whole degraded type, or a dynamic piece of a pushed-down one. A type + // that is also a slot of its own belongs to that slot, not to a container + // it is a piece of. + const owner = lowerer.degradedTypes.has(id) ? undefined : lowerer.lenientPieces.get(id); + if (!lowerer.degradedTypes.has(id) && owner === undefined) return null; + const text = ts.getTextOfNode(n); + const declaredType = lowerer.checker.typeToString(type); + return { + kind: "slot", + name: text.length > 40 ? `${text.slice(0, 37)}...` : text, + declaredType, + ...(owner !== undefined && owner !== declaredType ? { owner } : {}), + }; +} + /** Why a node's value has no static type, or null. */ function nodeCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null { if (isJsonParse(n)) return { kind: "json" }; + const slot = slotCause(lowerer, n); + if (slot !== null) return slot; const id = rootIdentifier(n); return id === null ? null : identifierCause(lowerer, id); } @@ -219,15 +251,28 @@ function nodeCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null { * the source does not say. The site's own subexpressions are asked first * (the operand of a dynamic `for...of`), then its ancestors. */ function causeAt(lowerer: Lowerer, loc: SrcLoc): UntypedCause | null { + return causeSiteAt(lowerer, loc)?.cause ?? null; +} + +/** causeAt, with the node that names the cause (none for an untyped + * JavaScript file). */ +function causeSiteAt( + lowerer: Lowerer, + loc: SrcLoc, +): { cause: UntypedCause; node?: ts.Node } | null { const sf = lowerer.program.getSourceFile(loc.file); if (sf === undefined) return null; if (isJsSourceFile(sf) && !isCheckedJsSourceFile(sf, lowerer.projectCheckJs)) - return { kind: "javascript" }; + return { cause: { kind: "javascript" } }; const node = nodeAt(sf, loc.start, loc.end); - let found: UntypedCause | null = null; + let found: { cause: UntypedCause; node: ts.Node } | null = null; + const ask = (n: ts.Node): void => { + const cause = nodeCause(lowerer, n); + if (cause !== null) found = { cause, node: n }; + }; const descend = (n: ts.Node, depth: number): void => { if (found !== null || depth > 3) return; - found = nodeCause(lowerer, n); + ask(n); if (found === null) ts.forEachChild(n, (child) => (descend(child, depth + 1), found !== null || undefined)); }; @@ -237,7 +282,7 @@ function causeAt(lowerer: Lowerer, loc: SrcLoc): UntypedCause | null { found === null && n !== undefined && n !== sf; n = n.parent ) - found = nodeCause(lowerer, n); + ask(n); return found; } @@ -254,6 +299,10 @@ function causeOf(lowerer: Lowerer, sites: readonly { loc: SrcLoc }[]): UntypedCa /** Generated functions (see scanDynamicTier), after any module prefix. */ const GENERATED = /^%*(?:m\d+\.)?%*(?:dyn|caught|init)\./; +/** Generated helpers that repeat their callers' locations: `%dyn.*` and + * `%caught.*`, but not a module's top level (`%init.*`). */ +const HELPERS = /^%*(?:m\d+\.)?%*(?:dyn|caught)\./; + /** The likely-hot dynamic-tier functions of `module` (see the header). */ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTierReport { const facts = new Map(); @@ -382,3 +431,69 @@ export function dynamicTierWarning( first.loc, ); } + +/** Localized failure: dynamic operations attributed to each dynamic slot + * of `slots` (by index), over the dynamic-tier functions of the program. A + * site counts for the slot the SC6101 cause search names at it: the slot + * declared by the binding the site reads (for a dynamic piece, the binding + * its access starts from), or else the one slot of the declared type. */ +export function dynamicSlotOperationCounts( + lowerer: Lowerer, + bodies: readonly IrFunction[], + slots: readonly DynamicSlot[], +): number[] { + const counts = slots.map(() => 0); + if (lowerer.degradedTypes.size === 0) return counts; + for (const fn of bodies) { + // Generated helpers repeat their callers' locations; top-level code + // (`%init.*`) counts like any function. + if (HELPERS.test(fn.name)) continue; + for (const site of dynamicOperationSites(fn)) { + const found = causeSiteAt(lowerer, site.loc); + if (found?.cause.kind !== "slot" || found.node === undefined) continue; + const declaredType = found.cause.owner ?? found.cause.declaredType; + const index = slotOfSite(lowerer, slots, found.node, declaredType, found.cause.owner); + if (index >= 0) counts[index]!++; + } + } + return counts; +} + +/** The index in `slots` of the slot a dynamic operation on `node` (an + * identifier or property access the cause search named) belongs to, or -1 + * when the declared type is not one slot's and no declaration says. */ +function slotOfSite( + lowerer: Lowerer, + slots: readonly DynamicSlot[], + node: ts.Node, + declaredType: string, + owner: string | undefined, +): number { + const candidates: number[] = []; + slots.forEach((slot, i) => { + if (slot.failure.declaredType === declaredType) candidates.push(i); + }); + if (candidates.length <= 1) return candidates[0] ?? -1; + // The binding: the identifier or property itself for a whole slot, the + // root of the access for a piece of a container. + const named = + owner !== undefined + ? rootIdentifier(node) + : ts.isPropertyAccessExpression(node) + ? node.name + : node; + if (named === null) return -1; + let symbol = lowerer.checker.getSymbolAtLocation(named); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + for (const decl of symbol?.declarations ?? []) { + const file = ts.getSourceFileOfNode(decl).fileName; + const start = ts.tokenStartOfNode(decl); + const index = candidates.find((i) => { + const loc = slots[i]!.loc; + return loc.file === file && loc.start >= start && loc.start < decl.end; + }); + if (index !== undefined) return index; + } + return -1; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 5854a0ae8..e3088f4a2 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -652,6 +652,17 @@ export function lowerArrayLiteral( return lowered; }); if (!needsStateBuild) { + // `[...text]`: the code points call already builds a fresh array of the + // literal's type, so it is the literal's value, as `Array.from(text)` is. + const only = elems[0]; + if ( + elems.length === 1 && + spreads.length === 1 && + only?.kind === "libCall" && + only.fn === "str.codePoints" && + typeEquals(only.type, type) + ) + return { ...only, loc }; return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc }; } const out = lowerer.declareHiddenLocal("%arrayLit", type); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index b0dcef332..5c326ab1e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -37,6 +37,7 @@ import { dynCompoundValue, lowerAbsenceProbe, lowerDynMemberAssignment, + lowerDereferencedElementAssignment, lowerOptionalNumber, lowerPresenceOperand, nullishBranches, @@ -334,6 +335,25 @@ function isMemberReceiver(node: ts.Expression): boolean { ); } +/** A literal default (`0`, `-1`, `""`, `true`): it always has its own type. + * Other defaults, such as an element read that can be undefined, keep the + * dynamic path. */ +function literalDefault(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if ( + ts.isPrefixUnaryExpression(node) && + (node.operator === ts.SyntaxKind.MinusToken || node.operator === ts.SyntaxKind.PlusToken) + ) + node = node.operand; + return ( + ts.isNumericLiteral(node) || + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + node.kind === ts.SyntaxKind.TrueKeyword || + node.kind === ts.SyntaxKind.FalseKeyword + ); +} + export function lowerNullishPair( lowerer: Lowerer, expr: ts.BinaryExpression, @@ -388,6 +408,26 @@ export function lowerNullishPair( lowerer.runtimeOptionalArithmeticTypes.set(expr, type); } if (type.kind === "dyn" && lowerer.dynConvertible(left.type)) { + // `counts.get(k) ?? 0` in JavaScript: a scalar optional left and a + // default of its value's type give a scalar, so the test and the default + // run on the typed path and only the result converts. Converting the + // optional left itself would allocate a union for every read. + if ( + rest.length === 1 && + primitive(rest[0]!) && + rightType && + typeEquals(rightType, rest[0]!) && + literalDefault(expr.right) + ) { + const typed: IrExpr = { + kind: "nullish", + left, + right: lowerer.lowerExprExpecting(expr.right, rest[0]!), + type: rest[0]!, + loc, + }; + return lowerer.coerceToExpected(typed, DYN); + } return { kind: "nullish", left: lowerer.coerceToExpected(left, DYN), @@ -774,6 +814,12 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr isUnitType(a), ) ?? false); + // 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" || @@ -1023,6 +1069,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (prototypeWrite) return prototypeWrite; const callableWrite = lowerNativeFunctionAssignment(lowerer, expr); if (callableWrite) return callableWrite; + const dereferenced = lowerDereferencedElementAssignment(lowerer, expr); + if (dereferenced) return dereferenced; } if ( op === ts.SyntaxKind.QuestionQuestionEqualsToken || diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 29f249282..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, @@ -465,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; @@ -597,6 +605,15 @@ 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); @@ -1319,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 @@ -1489,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; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 26014cfd9..1b21bf0c7 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"; @@ -55,6 +56,7 @@ import { varRef } from "../../../ir/build.js"; import { ambientUndefReadType, ambientUndefinedFnSymbolOf, + ambientUndefinedVarSymbolOf, contextualUndefReadType, fenceEarlyAliasUse, isTemporalGlobalRoot, @@ -89,7 +91,7 @@ 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 { isNavigatorGlobal, navigatorValue } from "../builtins/navigator.js"; import { moduleFileName } from "../import-meta.js"; export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr { @@ -318,10 +320,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(); } @@ -597,6 +601,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); @@ -606,12 +613,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, }; @@ -729,43 +731,11 @@ 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, @@ -860,14 +830,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 { @@ -1024,14 +987,6 @@ 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. @@ -1161,3 +1116,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 f2e388018..91b63cabc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -51,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 @@ -1586,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. @@ -1630,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 { @@ -1714,6 +1851,24 @@ function declBoundaryCast( ...(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 { @@ -1726,7 +1881,7 @@ 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 }), @@ -1737,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/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 1eaf3fb87..31479e4b0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -419,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(); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 6bdf9e2f0..58f07f890 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,6 +1,12 @@ 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"; @@ -38,7 +44,6 @@ import { } from "../../type-mapper.js"; import { BOOL, - BYTES_ELEMENT_NAME, BYTES_U8, DYN, F64, @@ -63,7 +68,7 @@ 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"; @@ -322,6 +327,8 @@ export function lowerPropertyAccessExpr( 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)) { @@ -612,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), @@ -621,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 @@ -995,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", @@ -3376,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", @@ -3651,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; @@ -3827,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; @@ -4304,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", @@ -4684,11 +4730,24 @@ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set + 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, @@ -535,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, @@ -820,9 +840,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (left.type.kind === "union") { const unionId = left.type.unionId; const arms = lowerer.unions.get(unionId)?.arms; - // Ordinary records, arrays and scalar values do not carry a class - // prototype (no program class extends Array). Dynamic/opaque runtime - // objects keep their existing instanceof fences. + // Records, arrays and scalar values are no instances of a class that + // does not extend Array (lowerArrayClassInstanceOf tests the classes + // that do). Dynamic/opaque runtime objects keep their existing + // instanceof fences. if ( arms?.every( (arm) => @@ -2271,3 +2292,20 @@ function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean ); }); } + +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 f6743768a..5f1e4a830 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -34,7 +34,11 @@ 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 { @@ -220,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 diff --git a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts index 4f4532a6a..01c841fdd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts +++ b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts @@ -281,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/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts index fc770a0cb..3700b57ff 100644 --- a/packages/compiler/src/frontend/lowering/frozen-records.ts +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -362,3 +362,37 @@ export function freezableRecordType(lowerer: Lowerer, type: IrType): type is Rec const shape = lowerer.shapes.get(type.shapeId); return shape !== undefined && !shapeHasAccessorSlots(shape); } + +/** The source ranges (writeKey) of untyped property writes in sloppy-mode + * code (IrModule.sloppyDynWrites): an assignment there that strict mode + * refuses with a TypeError (a frozen or non-extensible object, a read-only + * property) leaves the object unchanged instead. Only programs with such + * writes list any; the backend calls the sloppy entry points for them. */ +export function sloppyDynamicWrites(lowerer: Lowerer, functions: readonly IrFunction[]): string[] { + const keys = new Set(); + const strictness = new Map(); + for (const fn of functions) { + everyStmtList(fn.body, { + stmt: () => true, + expr: (e) => { + if (e.kind !== "libCall" || (e.fn !== "dyn.keySet" && e.fn !== "dyn.keySetComputed")) + return true; + const key = writeKey(e.loc); + let sloppy = strictness.get(key); + if (sloppy === undefined) { + sloppy = isSloppyAt(lowerer, e.loc); + strictness.set(key, sloppy); + } + if (sloppy) keys.add(key); + return true; + }, + }); + } + return [...keys].sort(); +} + +/** Whether the source at `loc` is sloppy-mode code (isStrictCode). */ +export function isSloppyAt(lowerer: Lowerer, loc: SrcLoc): boolean { + const node = lowerer.nodeAtLoc(loc); + return node !== null && !isStrictCode(node); +} diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index db542412f..459af8751 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -1,7 +1,8 @@ -import { dynUndefinedExpr, varRef } from "../../ir/build.js"; +import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import { BOOL, DYN, + F64, STRING, canConvertToDyn, funcOf, @@ -16,7 +17,7 @@ import { import { transformStmtList } from "../../ir/traverse.js"; import { genResultRecord } from "../type-mapper.js"; import type { DispatchSites } from "./dispatch-sites.js"; -import type { Lowerer } from "./lowerer.js"; +import type { Lowerer, UntypedLoopSite } from "./lowerer.js"; type GeneratorType = Extract; type Invoke = Extract; @@ -401,6 +402,310 @@ export class GeneratorDynamicDispatch { }); changed = true; } + return this.stepUntypedLoops(lowerer, functions) || changed; + } + + /** The generator types a loop over an untyped iterable can step natively: + * sync generators for `for … of`, and both kinds for `for await` (a sync + * generator through the async-from-sync rules). Insertion order, so a + * type keeps its index as later waves add types. */ + private loopTypes(lowerer: Lowerer, site: UntypedLoopSite): [string, GeneratorType][] { + const out: [string, GeneratorType][] = []; + for (const [key, type] of this.generators) { + if (type.async === true && !site.async) continue; + const result = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (result === null) continue; + // next() with no argument, as the protocol calls it. + const channel = type.nextT; + if (channel.kind !== "undefinedT" && channel.kind !== "void" && channel.kind !== "dyn") + continue; + if ( + !canConvertToDyn( + result, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + continue; + out.push([key, type]); + } + return out; + } + + /** Loops over untyped iterables step the program's own generators on the + * typed path. Once the loop has its iterator and next method, a kind local + * records which generator type the iterator is, when its next method is + * the generator's own (`%dyn.generator.method.next`); each step then + * resumes the generator directly and boxes only the value, where the + * protocol would box the result record and its promise and read them + * back. Every other iterator, and every early exit (return() through the + * protocol), keeps the dynamic path. The awaits are the protocol's: a + * `for await` over an async generator awaits next()'s promise, and over a + * sync generator awaits once for the result and, reached through + * Symbol.iterator, once for the value. */ + private stepUntypedLoops(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + if (this.generators.size === 0 || lowerer.untypedLoops.size === 0) return false; + // The loop reads next through the getter only once it exists. + if (!this.properties.has("next")) return false; + let changed = false; + for (const fn of functions) { + if (this.generated.has(fn)) continue; + const byKind = new Map(); + const byValue = new Map(); + for (const local of fn.locals) { + const site = lowerer.untypedLoops.get(local); + if (site === undefined) continue; + byKind.set(site.kind, site); + byValue.set(site.value, site); + } + if (byKind.size === 0) continue; + fn.body = transformStmtList(fn.body, { + expr: (expr) => expr, + stmt: (stmt) => { + if (stmt.kind !== "varDecl" || stmt.init === null) return stmt; + const opened = byKind.get(stmt.localId); + if (opened !== undefined) { + const types = this.loopTypes(lowerer, opened); + const init = this.loopKind(opened, types, stmt.loc); + if (this.loopKindCount(stmt.init) === types.length) return stmt; + changed = true; + return { ...stmt, init }; + } + const site = byValue.get(stmt.localId); + if (site === undefined) return stmt; + const types = this.loopTypes(lowerer, site); + // The protocol step is the innermost else of the type chain. + let fallback = stmt.init; + let present = 0; + while ( + fallback.kind === "ternary" && + fallback.cond.kind === "bin" && + fallback.cond.left.kind === "varRef" && + fallback.cond.left.localId === site.kind + ) { + fallback = fallback.else_; + present++; + } + if (present === types.length) return stmt; + let init = fallback; + for (let i = types.length - 1; i >= 0; i--) { + const [, type] = types[i]!; + const record = `${site.kind}.r${i + 1}`; + init = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: varRef(site.kind, F64, stmt.loc), + right: numLit(i + 1, stmt.loc), + type: BOOL, + loc: stmt.loc, + }, + then: this.loopStep(lowerer, fn, site, type, record, stmt.loc), + else_: init, + type: DYN, + loc: stmt.loc, + }; + } + changed = true; + return { ...stmt, init }; + }, + }); + } return changed; } + + /** How many generator types a loop's kind initializer tests: the chain + * inside its own-method test (loopKind). */ + private loopKindCount(init: IrExpr): number { + if (init.kind !== "ternary") return 0; + let count = 0; + for (let chain = init.then; chain.kind === "ternary"; chain = chain.else_) count++; + return count; + } + + /** The loop's kind: the 1-based index of the generator type its iterator + * is, when the next method it read is that generator's own, else 0. */ + private loopKind( + site: UntypedLoopSite, + types: readonly [string, GeneratorType][], + loc: IrExpr["loc"], + ): IrExpr { + const own: IrExpr = { + kind: "libCall", + fn: "dyn.sameValue", + args: [ + varRef(site.next, DYN, loc), + { + kind: "dynFrom", + value: { + kind: "closure", + fnName: "%dyn.generator.method.next", + captures: [], + type: funcOf([DYN], DYN), + loc, + }, + type: DYN, + loc, + }, + ], + type: BOOL, + loc, + }; + let kind: IrExpr = numLit(0, loc); + for (let i = types.length - 1; i >= 0; i--) + kind = { + kind: "ternary", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [ + varRef(site.iterator, DYN, loc), + { kind: "strLit", value: types[i]![0], type: STRING, loc }, + ], + type: BOOL, + loc, + }, + then: numLit(i + 1, loc), + else_: kind, + type: F64, + loc, + }; + return types.length === 0 + ? kind + : { kind: "ternary", cond: own, then: kind, else_: numLit(0, loc), type: F64, loc }; + } + + /** One step of a loop over an untyped iterable that is a `type` + * generator: sets the loop's done flag and answers the value, boxed. */ + private loopStep( + lowerer: Lowerer, + fn: IrFunction, + site: UntypedLoopSite, + type: GeneratorType, + recordId: string, + loc: IrExpr["loc"], + ): IrExpr { + const gen: IrExpr = { kind: "dynCheck", value: varRef(site.iterator, DYN, loc), type, loc }; + const setDone = (value: IrExpr): IrStmt => ({ kind: "assign", localId: site.done, value, loc }); + const doneRef = varRef(site.done, BOOL, loc); + const result = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions)!; + if (!fn.locals.some((local) => local.id === recordId)) + fn.locals.push({ id: recordId, name: recordId, type: result, mutable: false }); + const field = (name: "value" | "done", fieldType: IrType): IrExpr => ({ + kind: "recordGet", + obj: varRef(recordId, result, loc), + shapeId: result.shapeId, + field: name, + type: fieldType, + loc, + }); + const valueType = lowerer.shapes + .get(result.shapeId)! + .fields.find((f) => f.name === "value")!.type; + const boxed: IrExpr = { kind: "dynFrom", value: field("value", valueType), type: DYN, loc }; + const awaitDyn = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "async.awaitDyn", + args: [value], + type: DYN, + loc, + }); + const resumed: IrExpr = { + kind: "genResume", + mode: "next", + gen, + arg: null, + type: type.async === true ? { kind: "promise", inner: result } : result, + loc, + }; + if (!site.async) + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: recordId, init: resumed, loc }, + setDone(field("done", BOOL)), + ], + result: { + kind: "ternary", + cond: doneRef, + then: dynUndefinedExpr(loc), + else_: boxed, + type: DYN, + loc, + }, + type: DYN, + loc, + }; + if (type.async === true) + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: recordId, + init: { kind: "awaitExpr", value: resumed, type: result, loc }, + loc, + }, + setDone(field("done", BOOL)), + ], + result: { + kind: "ternary", + cond: doneRef, + then: dynUndefinedExpr(loc), + else_: boxed, + type: DYN, + loc, + }, + type: DYN, + loc, + }; + // for await over a sync generator: the result awaits once. When the + // loop reached it through Symbol.iterator, the async-from-sync iterator + // also awaits the value (the final one too); a sync generator that an + // object's Symbol.asyncIterator returns passes the value as it is, as + // the protocol does. + const fromSync: IrExpr = + site.sync === null + ? { kind: "boolLit", value: false, type: BOOL, loc } + : varRef(site.sync, BOOL, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: recordId, init: resumed, loc }, + setDone(field("done", BOOL)), + { kind: "exprStmt", expr: awaitDyn(dynUndefinedExpr(loc)), loc }, + ], + result: { + kind: "ternary", + cond: doneRef, + then: { + kind: "ternary", + cond: fromSync, + then: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: awaitDyn(boxed), loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: { + kind: "ternary", + cond: fromSync, + then: awaitDyn(boxed), + else_: boxed, + type: DYN, + loc, + }, + type: DYN, + loc, + }, + type: DYN, + loc, + }; + } } diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 1769e8562..046b34008 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,7 +1,10 @@ +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"; import * as ts from "../ts7/adapter.js"; +import { heritageElementTarget, isHeritageTypeReference } from "../heritage-elements.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -653,8 +656,9 @@ function extendsParameter( } for (const clause of decl.heritageClauses?.nodes ?? []) for (const base of clause.types.nodes) { - const symbol = ts.isIdentifier(base.expression) - ? lowerer.checker.getSymbolAtLocation(base.expression) + const target = heritageElementTarget(base); + const symbol = ts.isIdentifier(target) + ? lowerer.checker.getSymbolAtLocation(target) : undefined; if (symbol !== undefined && params.has(symbol)) return true; } @@ -1650,7 +1654,7 @@ export function implicitMonoFile(sf: ts.SourceFile): boolean { * parameter symbol — assignment, compound assignment, ++/--, a * destructuring-assignment target, or a for-in/of cursor. A written * param's binding could lie after the write, so it stays dyn. */ -function paramWrittenInBody( +export function paramWrittenInBody( lowerer: Lowerer, body: ts.Node, sym: ts.Symbol, @@ -1863,10 +1867,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); @@ -2370,18 +2378,46 @@ export function implicitDefaultInstance( return internImplicitInstance(lowerer, blame, info, shapes, argTypes); } -function internImplicitInstance( +export function internImplicitInstance( lowerer: Lowerer, blame: ts.Node, info: GenericFnInfo, shapes: ParamShape[], argTypes: Map, patternKey = "", + declaredReturn?: IrType, ): GenericInstance { + return internImplicitInstanceOr( + lowerer, + blame, + info, + shapes, + argTypes, + patternKey, + declaredReturn, + false, + )!; +} + +/** internImplicitInstance; with `fallible`, a body that does not lower + * leaves no instance and no diagnostics, and the answer is null (the caller + * keeps its own lowering). */ +export function internImplicitInstanceOr( + lowerer: Lowerer, + blame: ts.Node, + info: GenericFnInfo, + shapes: ParamShape[], + argTypes: Map, + patternKey: string, + declaredReturn: IrType | undefined, + fallible: boolean, +): GenericInstance | null { const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : ""); let inst = info.instances.get(key); - if (inst?.implicitState === "failed") + if (inst?.implicitState === "failed") { + if (fallible) return null; lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`); + } if (inst) { // A recursive caller has observed this ABI before inference settles. // Keep the promised return slot and coerce the completed body's returns. @@ -2405,7 +2441,8 @@ function internImplicitInstance( shapes[index]?.type.kind === "dyn" && broadPromiseParam(lowerer, lowerer.typeOf(param.name)), ); - const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); + const declared = + declaredReturn ?? (dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info)); inst = { path, name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), @@ -2426,6 +2463,25 @@ function internImplicitInstance( // JS body must still have a throwing implementation: later references // (including recursive ones) can already hold its cached signature. const diagsBefore = lowerer.diags.length; + const fencesBefore = lowerer.runtimeFences.length; + const totalBefore = lowerer.stats.statementsTotal; + const failedBefore = lowerer.stats.statementsFailed; + const skippedBefore = lowerer.stats.functionsSkipped; + const blockedBefore = fallible ? new Set(lowerer.blockedBindings) : null; + const deferredBefore = fallible ? new Set(lowerer.deferredDiags.keys()) : null; + /** Undoes what a failed fallible body recorded: the instance's + * statements share their symbols with the declaration's own body. */ + const rollback = (): void => { + lowerer.diags.splice(diagsBefore); + lowerer.runtimeFences.splice(fencesBefore); + lowerer.stats.statementsTotal = totalBefore; + lowerer.stats.statementsFailed = failedBefore; + lowerer.stats.functionsSkipped = skippedBefore; + for (const symbol of [...lowerer.blockedBindings]) + if (!blockedBefore!.has(symbol)) lowerer.blockedBindings.delete(symbol); + for (const symbol of [...lowerer.deferredDiags.keys()]) + if (!deferredBefore!.has(symbol)) lowerer.deferredDiags.delete(symbol); + }; // The instance is cached even when a receiver probe requested it. // Its body must retain statement fences instead of dropping failures // into that probe's temporary diagnostic sink and caching empty code. @@ -2445,10 +2501,22 @@ function internImplicitInstance( ...(info.decl.body ? [info.decl.body] : []), ]); const fn = lowerer.lowerGenericInstance(info, inst); + if ( + fallible && + (lowerer.diags.length > diagsBefore || lowerer.runtimeFences.length > fencesBefore) + ) { + inst.implicitState = "failed"; + rollback(); + return null; + } lowerer.implicitFns.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; inst.implicitState = "failed"; + if (fallible) { + rollback(); + return null; + } const types: IrType[] = shapes.map((shape) => shape.type); if (info.member?.kind === "method") types.unshift({ kind: "object", className: info.member.cls.def.name }); @@ -2970,7 +3038,12 @@ function bindingReferences( resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved; if (resolved !== symbol) return false; for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) { - if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false; + if ( + ts.isTypeNode(p) && + p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments && + !isHeritageTypeReference(p) + ) + return false; if (ts.isStatement(p) || ts.isFunctionLike(p)) break; } return true; 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/handle-properties.test.ts b/packages/compiler/src/frontend/lowering/handle-properties.test.ts new file mode 100644 index 000000000..3c1d83ac3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/handle-properties.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "vitest"; +import { VOID, type IrExpr, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js"; +import { applyHandleProperties } from "./handle-properties.js"; +import { ENTRY_NAME, type Lowerer } from "./lowerer.js"; + +const loc: SrcLoc = { file: "main.ts", start: 0, end: 1 }; +const fn = (name: string, body: IrStmt[]): IrFunction => ({ + name, + body, + params: [], + locals: [], + returnType: VOID, + loc, +}); + +test("renames a libCall the IR reaches from several statements once", () => { + const call: IrExpr = { kind: "libCall", fn: "dyn.defineProperty", args: [], type: VOID, loc }; + const use: IrStmt = { kind: "exprStmt", expr: call, loc }; + const functions = [fn(ENTRY_NAME, [use, use]), fn("helper", [use])]; + applyHandleProperties({ writesOntoChildren: true } as unknown as Lowerer, functions); + expect(call).toMatchObject({ kind: "libCall", fn: "child.propsDefine" }); + expect(functions[0]!.body[0]).toMatchObject({ + kind: "exprStmt", + expr: { kind: "libCall", fn: "child.propsInstall" }, + }); +}); diff --git a/packages/compiler/src/frontend/lowering/handle-properties.ts b/packages/compiler/src/frontend/lowering/handle-properties.ts index 4af4ca54d..ba098aab5 100644 --- a/packages/compiler/src/frontend/lowering/handle-properties.ts +++ b/packages/compiler/src/frontend/lowering/handle-properties.ts @@ -283,10 +283,11 @@ const RENAMES: ReadonlyMap = new Map([ * property-table entries (in place) and installs the table in %main. */ export function applyHandleProperties(lowerer: Lowerer, functions: IrFunction[]): void { if (!lowerer.writesOntoChildren && !childAdditions(lowerer).any) return; - const calls: (IrExpr & { kind: "libCall" })[] = []; + // A node the IR shares at several places is renamed once. + const calls = new Set(); const visitor = { expr: (e: IrExpr): boolean => { - if (e.kind === "libCall" && RENAMES.has(e.fn)) calls.push(e); + if (e.kind === "libCall" && RENAMES.has(e.fn)) calls.add(e); return true; }, stmt: (): boolean => true, diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7aed5a271..5f151c7a9 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,9 @@ +import { + arrayClassMemberOwner, + arrayClassReceiver, + lowerArrayClassStaticCall, +} from "./builtins/array-classes.js"; +import { structuralCallInstance } from "./structural-instances.js"; import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js"; import { assertFunctionAliasOf } from "./lower-assert.js"; import { keyedHelperName } from "./stable-names.js"; @@ -1355,6 +1361,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { sig && !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments.nodes)) ) { + const layout = structuralCallInstance(lowerer, expr, sig); + if (layout) { + return reconcileOverloadReturn(lowerer, expr, { + kind: "call", + callee: layout.name, + args: lowerer.completeArgs(expr.arguments.nodes, layout.params, loc, expr), + type: layout.returnType, + loc, + }); + } lowerer.noteEdge(sig.name); const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); @@ -1737,6 +1753,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) ?? @@ -8707,7 +8724,13 @@ function lowerObjectStaticCall( 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 + // A fresh literal can still reach untyped code, which observes and + // must honor its frozen state: records and arrays keep the bit. + 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)); + return value; } if ( value.type.kind === "string" || @@ -10181,6 +10204,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) ?? @@ -10214,6 +10243,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, @@ -10235,8 +10271,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. @@ -10589,11 +10625,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; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d941d7b60..80919a251 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,5 @@ import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js"; +import { shapeOverlayAnnotated } from "../shape-inference/index.js"; import { DATE_TIME_FIELD, dateRootOf, @@ -7,6 +8,14 @@ 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"; @@ -116,6 +125,7 @@ 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"; @@ -606,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. */ @@ -1168,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). @@ -1994,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; @@ -2194,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; @@ -2294,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(); } } @@ -2537,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`, ); @@ -3053,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`, ); @@ -3196,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`, ); @@ -3332,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 && @@ -4400,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 && @@ -4466,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, @@ -4482,6 +4540,7 @@ export function collectClassShapeInner( ...(genericBase ? { genericBase } : {}), ...(callableBase ? { callableBase } : {}), ...(extendsDate ? { extendsDate: true as const } : {}), + ...(extendsArray ? { extendsArray } : {}), ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, @@ -6205,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); @@ -6219,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(); @@ -7639,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)) && @@ -7725,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), ); } @@ -7976,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))) { @@ -8024,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)); @@ -8091,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 @@ -8103,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 @@ -9150,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", @@ -9343,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", @@ -11315,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, @@ -11850,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-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 006975085..89ea52bc5 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,9 @@ +import type { DiagnosticCode } from "../../diagnostics/codes.js"; +import { + arrayClassMemberOwner, + arrayOfClassInstance, + arrayRootOf, +} from "./builtins/array-classes.js"; import { keyedHelperName } from "./stable-names.js"; import { isNewTarget, @@ -77,6 +83,8 @@ import { unitOnlyUnion } from "../type-mapper.js"; import { lowerYield } from "./lower-generators.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { everyExprChild } from "../../ir/traverse.js"; +import { declareContextLocal } from "./function-context.js"; import { hasOptionalChainGuard, isOptionalChainTail } from "./expressions/optional-chains.js"; import { classToStringConversion, classUnionToString } from "./class-to-primitive.js"; import { lowerCjsTopLevelThis, lowerRequireCacheElement } from "./lower-node-module.js"; @@ -96,7 +104,8 @@ import { 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, @@ -406,6 +415,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const probe = lowerAbsenceProbe(lowerer, expr.expression); if (probe?.type.kind === "union") return probe; } + const dereferenced = dereferencedElementReceiver(lowerer, expr, loc); + if (dereferenced) return dereferenced; + const element = assertedElementOperand(lowerer, expr, loc); + if (element) return element; const inner = lowerer.lowerExpr(expr.expression); if (inner.type.kind === "union") { // `!` performs no runtime check: a missing array element stays @@ -508,7 +521,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). @@ -584,7 +613,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 @@ -771,7 +817,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const entry = UNSUPPORTED_EXPR[expr.kind]; if (entry) lowerer.unsupported( - entry.code as `SC${number}` & keyof typeof UNSUPPORTED, + entry.code as DiagnosticCode & keyof typeof UNSUPPORTED, expr, entry.feature, ); @@ -1035,18 +1081,10 @@ function assertionOperand( 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). + if (!scalarUnion(lowerer, inner.type.unionId)) return null; + // Array element reads take assertedElementOperand or 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)) { @@ -1055,19 +1093,513 @@ function assertionOperand( } const use = convertingUseOf(lowerer, expr); if (use === null) return null; - if (use === "value") return inner; + if (use === "value" || use === "string") 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 +/** `xs[i]!` on an array as the operand of a converting use (see + * assertionOperand). The read answers undefined for a missing element, as + * in Node, instead of trapping: a numeric use reads NaN for it, and other + * uses get the optional element value. Returns null for other reads and * uses. */ -function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" | "value" | null { +function assertedElementOperand( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, +): IrExpr | null { + let read: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(read)) read = read.expression; + if (!ts.isElementAccessExpression(read) || read.questionDotToken) return null; + // Plain arrays only: regex match results and other array subtypes keep + // their own element reads. + const receiverType = lowerer.typeOf(read.expression); + if ( + !lowerer.checker.isArrayType(receiverType) || + lowerer.mapTypeOf(receiverType)?.kind !== "array" + ) + return null; + // A counter below the length reads an existing index: the read keeps + // its plain element access. + if (lowerer.nanCodedReads?.counterIndexInBounds(read)) return null; + const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (!target || !isScalarArm(target)) return null; + const use = convertingUseOf(lowerer, expr); + if (use === null) return null; + if (use === "string" && target.kind === "string") + return stringElementOrUndefined(lowerer, read, loc); + const probe = lowerAbsenceProbe(lowerer, read); + if (probe?.type.kind !== "union" || !scalarUnion(lowerer, probe.type.unionId)) return null; + if (use === "value" || use === "string") return probe; + return lowerer.stripUndefinedArm(probe.type).kind === "f64" + ? lowerOptionalNumber(lowerer, probe, loc) + : lowerPrimitiveUnionNumber(lowerer, probe, expr, loc); +} + +/** `xs[i]!` on an array as the receiver of a member read + * (`xs[i]!.name`, `xs[i]!.size()`, `xs[i]![0]`). `!` performs no runtime + * check, so a missing element is undefined there and the member access + * throws Node's TypeError ("Cannot read properties of undefined (reading + * 'name')") instead of the bounds trap. The receiver is the element when it + * is present. A write (`xs[i]!.name = v`) is lowered as a statement by + * lowerDereferencedElementWrite, which evaluates the value before the + * receiver throws; elsewhere it keeps the bounds trap. Returns null for + * other reads and uses. */ +function dereferencedElementReceiver( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, +): IrExpr | null { + const use = runtimeOptionalUseOf(expr); + if (!use || isAssignmentTarget(use.access)) return null; + const parts = dereferencedElementParts(lowerer, expr, loc, false); + if (parts?.message == null) return null; + const message = parts.message; + const value: IrExpr = { + kind: "ternary", + cond: parts.present, + then: parts.element, + else_: nodeTypeError(message, parts.element.type, loc), + type: parts.element.type, + loc, + }; + return parts.stmts.length === 0 + ? value + : { kind: "seqExpr", stmts: parts.stmts, result: value, type: value.type, loc }; +} + +/** The parts of a member access through `xs[i]!` (see + * dereferencedElementReceiver): statements that evaluate the array and the + * index, whether the element is present, the element read, and Node's + * TypeError message for a missing one. A `write` holds its key, so its + * message may be left for dereferencedElementWrite to build from the key. */ +export function dereferencedElementParts( + lowerer: Lowerer, + expr: ts.NonNullExpression, + loc: SrcLoc, + write: boolean, +): { stmts: IrStmt[]; present: IrExpr; element: IrExpr; message: IrExpr | null } | null { + let read: ts.Expression = expr.expression; + while (ts.isParenthesizedExpression(read)) read = read.expression; + if (!ts.isElementAccessExpression(read) || read.questionDotToken) return null; + const receiverType = lowerer.typeOf(read.expression); + if ( + !lowerer.checker.isArrayType(receiverType) || + lowerer.mapTypeOf(receiverType)?.kind !== "array" + ) + return null; + if (lowerer.nanCodedReads?.counterIndexInBounds(read)) return null; + const use = runtimeOptionalUseOf(expr); + if (!use || use.optional || use.complex || use.comma || use.kind === "call") return null; + // A write holds its key, so the message can name any number or string + // key (dereferencedElementWrite builds it). + const message = + memberAccessMessage(lowerer, use.access) ?? + (write && ts.isElementAccessExpression(use.access) && namedKey(lowerer, use.access) + ? null + : undefined); + if (message === undefined) return null; + if (lowerer.mapTypeOf(lowerer.typeOf(read.argumentExpression))?.kind !== "f64") return null; + // The element must already have the asserted type: an element union the + // assertion or a guard narrows keeps its checked narrow. + const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const declared = lowerer.mapTypeOf(receiverType); + if ( + !target || + declared?.kind !== "array" || + declared.elem.kind === "void" || + !typeEquals(declared.elem, target) + ) + return null; + const arr = lowerer.lowerExpr(read.expression); + if (arr.type.kind !== "array" || !typeEquals(arr.type.elem, target)) return null; + const elem = arr.type.elem; + const index = lowerer.lowerExprExpecting(read.argumentExpression, F64); + // Plain reads (a local, a field of one) evaluate once more instead of + // through a hidden local: the receiver then stays a borrowed element + // read, like the plain `xs[i]` it replaces. An index that can assign + // can replace the array binding or field (`xs[(xs = other, 0)]!`), so the + // array is held before it, as JavaScript reads it first. + const stmts: IrStmt[] = []; + const plain = (value: IrExpr): boolean => isSafeToRepeat(value) || plainProjection(value); + const held = (value: IrExpr, name: string): IrExpr => { + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + const a = !mayAssign(index) && plain(arr) ? arr : held(arr, "%elementArray"); + const i = plain(index) || repeatableIndex(index) ? index : held(index, "%elementIndex"); + return { + stmts, + present: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: a, index: i, type: F64, loc }, + right: numLit(1, loc), + type: BOOL, + loc, + }, + element: { kind: "arrayGet", arr: a, index: i, type: elem, loc }, + message, + }; +} + +/** A write through `xs[i]!` (`xs[i]!.name = v`, `xs[i]![k] = v`): as in + * Node, the array, the index, the key and the value evaluate in order (the + * statements), then a missing element makes the write throw Node's + * TypeError and a present one takes the write, which reads the element, + * the key and the value through `overrides`. Null for other targets. */ +export function dereferencedElementWrite( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): { + stmts: IrStmt[]; + present: IrExpr; + message: IrExpr; + overrides: { node: ts.Expression; value: IrExpr }[]; +} | null { + if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; + const target = expr.left; + if ( + !(ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) || + target.questionDotToken || + lowerer.chainRecvByNode.has(target.expression) + ) + return null; + let receiver: ts.Expression = target.expression; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + if (!ts.isNonNullExpression(receiver)) return null; + const loc = locOf(expr); + const keyValue = ts.isElementAccessExpression(target) + ? lowerer.lowerExpr(target.argumentExpression) + : null; + const rightValue = lowerer.lowerExpr(expr.right); + const parts = dereferencedElementParts(lowerer, receiver, loc, true); + if (parts === null) return null; + const stmts = [...parts.stmts]; + let present = parts.present; + let element = parts.element; + // JavaScript reads the element before the key and the value. When either + // has an effect, which can replace, remove or add the element, its + // presence and value are read first; otherwise the write tests and reads + // it after them. + if (!unobservable(rightValue) || (keyValue !== null && !unobservable(keyValue))) { + const presentLocal = lowerer.declareHiddenLocal("%elementPresent", BOOL); + // Assigned only when the element is present. + const elementLocal = declareContextLocal( + lowerer.ctx, + "%elementValue", + element.type, + true, + undefined, + undefined, + ); + stmts.push( + { kind: "varDecl", localId: presentLocal.id, init: present, loc }, + { kind: "varDecl", localId: elementLocal.id, init: null, loc }, + ); + present = varRef(presentLocal.id, BOOL, loc); + stmts.push({ + kind: "if", + cond: present, + then: [{ kind: "assign", localId: elementLocal.id, value: element, loc }], + else_: null, + loc, + }); + element = varRef(elementLocal.id, element.type, loc); + } + const overrides: { node: ts.Expression; value: IrExpr }[] = []; + // The write lowers the node with or without its parentheses. + const override = (node: ts.Expression, value: IrExpr): void => { + for (let n: ts.Expression = node; ; n = n.expression) { + overrides.push({ node: n, value }); + if (!ts.isParenthesizedExpression(n)) break; + } + }; + const held = (value: IrExpr, name: string, inline: boolean): IrExpr => { + if (inline) return value; + // A call without a result evaluates to undefined. + if (value.type.kind === "void") { + stmts.push({ kind: "exprStmt", expr: value, loc }); + return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; + } + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + override(target.expression, element); + let message = parts.message; + if (ts.isElementAccessExpression(target) && keyValue !== null) { + // The key evaluates before the value, which can assign the binding or + // field it reads; a literal, or an unobservable key the value cannot + // change, may evaluate in the write. + const key = held( + keyValue, + "%elementWriteKey", + keyValue.kind === "numLit" || + keyValue.kind === "strLit" || + (unobservable(keyValue) && !mayAssign(rightValue)), + ); + override(target.argumentExpression, key); + // The message names the key's value as the write evaluated it. + if (key !== keyValue || message === null) + message = keyMessage(messageHead(target), key, loc) ?? message; + } + // A value whose evaluation is unobservable may evaluate in the write + // itself, after the element is tested. + override( + expr.right, + held(rightValue, "%elementWriteValue", isSafeToRepeat(rightValue) || unobservable(rightValue)), + ); + if (message === null) return null; + return { stmts, present, message, overrides }; +} + +/** dereferencedElementWrite in value position (`f(xs[i]!.name = v)`). */ +export function lowerDereferencedElementAssignment( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const write = dereferencedElementWrite(lowerer, expr); + if (write === null) return null; + const loc = locOf(expr); + const value = lowerer.withExpressionOverrides(write.overrides, () => lowerer.lowerExpr(expr)); + return { + kind: "seqExpr", + stmts: write.stmts, + result: { + kind: "ternary", + cond: write.present, + then: value, + else_: nodeTypeError(write.message, value.type, loc), + type: value.type, + loc, + }, + type: value.type, + loc, + }; +} + +/** Whether evaluating `e` has no effect and cannot throw: a discardable + * expression, or the compiler's element-or-undefined read of one. */ +function unobservable(e: IrExpr): boolean { + if (e.kind === "call" && e.callee.startsWith("%arr.idxOr.")) return e.args.every(unobservable); + return isSafeToDiscard(e); +} + +/** Whether evaluating `e` can assign a binding or a field: it contains an + * assignment, an increment, statements, a suspension, or a call that can + * run program code (a library call only through a function it is given). */ +function mayAssign(e: IrExpr): boolean { + switch (e.kind) { + case "call": + // The compiler's element-or-undefined read and union narrows run no + // program code. + if (!e.callee.startsWith("%arr.idxOr.") && !e.callee.startsWith("%union.narrow.")) + return true; + break; + case "assignExpr": + case "incDec": + case "seqExpr": + case "callValue": + case "virtualCall": + case "dynCall": + case "dynInvoke": + case "ffiCall": + case "new": + case "newValue": + case "awaitExpr": + case "awaitUnionExpr": + case "yieldExpr": + return true; + case "closure": + return false; + case "libCall": + if (e.args.some((arg) => arg.type.kind === "func")) return true; + break; + } + return !everyExprChild( + e, + (child) => !mayAssign(child), + () => false, + ); +} + +/** Node's TypeError, thrown in place of a value of `type`. */ +export function nodeTypeError(message: IrExpr, type: IrType, loc: SrcLoc): IrExpr { + return { + kind: "libCall", + fn: "error.nodeThrow", + args: [numLit(1, loc), strLit("", loc), message], + type, + loc, + }; +} + +/** Whether `access` is the target of `=`. */ +function isAssignmentTarget(access: ts.Expression): boolean { + let outer: ts.Expression = access; + while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const parent = outer.parent; + return ( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken + ); +} + +/** A local, a number literal, or a field or record member read through a + * local: reading it again has no effect and yields the same value. */ +function plainProjection(e: IrExpr): boolean { + if (e.kind === "varRef" || e.kind === "numLit") return true; + return (e.kind === "fieldGet" || e.kind === "recordGet") && plainProjection(e.obj); +} + +/** A number computed from plain reads, number literals and the lengths of + * arrays read through them (`xs.length - 1`): evaluating it again, with + * nothing in between, has no effect and yields the same value. An element + * read through `xs[xs.length - 1]!` then evaluates its index twice instead + * of holding it in a hidden local, and stays a borrowed element read. */ +function repeatableIndex(e: IrExpr): boolean { + if (e.type.kind !== "f64") return false; + switch (e.kind) { + case "numLit": + case "varRef": + case "fieldGet": + case "recordGet": + return plainProjection(e); + case "bin": + return repeatableIndex(e.left) && repeatableIndex(e.right); + case "unary": + return repeatableIndex(e.operand); + case "arrIntrinsic": + return e.method === "length" && e.args.length === 0 && plainProjection(e.receiver); + default: + return false; + } +} + +/** Node's TypeError message for a member access on undefined: reading the + * member, or setting it when the access is the target of `=`. Null when + * the key is not a name, a literal, or a plain read of a number or string + * (which the message reads a second time). */ +function memberAccessMessage( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression, +): IrExpr | null { + const head = messageHead(access); + const loc = locOf(access); + if (ts.isPropertyAccessExpression(access)) { + if (ts.isPrivateIdentifier(access.name)) return null; + return strLit(`${head}${access.name.text}')`, loc); + } + if (!ts.isElementAccessExpression(access)) return null; + const key = runtimeOptionalElementKey(access.argumentExpression); + if (key !== null) return strLit(`${head}${key}')`, loc); + let keyNode = access.argumentExpression; + while (ts.isParenthesizedExpression(keyNode)) keyNode = keyNode.expression; + if (!ts.isIdentifier(keyNode)) return null; + const keyType = lowerer.mapTypeOf(lowerer.typeOf(keyNode))?.kind; + if (keyType !== "f64" && keyType !== "string") return null; + const keyValue = lowerer.lowerExpr(keyNode); + if (!isSafeToRepeat(keyValue) || keyValue.type.kind !== keyType) return null; + return keyMessage(head, keyValue, loc); +} + +/** The start of Node's TypeError message for a member access on undefined: + * setting the member when the access is the target of `=`, else reading + * it. */ +function messageHead(access: ts.Expression): string { + return isAssignmentTarget(access) + ? "Cannot set properties of undefined (setting '" + : "Cannot read properties of undefined (reading '"; +} + +/** Whether `access`'s key is typed a number or a string, which the message + * can name from its value. */ +function namedKey(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { + const kind = lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression))?.kind; + return kind === "f64" || kind === "string"; +} + +/** The message naming a key from its value (a number or a string), or null + * for another type. */ +function keyMessage(head: string, key: IrExpr, loc: SrcLoc): IrExpr | null { + if (key.type.kind !== "f64" && key.type.kind !== "string") return null; + const text: IrExpr = + key.type.kind === "string" ? key : { kind: "toString", operand: key, type: STRING, loc }; + const concat = (left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "strConcat", + left, + right, + type: STRING, + loc, + }); + return concat(concat(strLit(head, loc), text), strLit("')", loc)); +} + +/** A string array's element converted to a string: the element, or + * "undefined" for a missing index or a hole. */ +function stringElementOrUndefined( + lowerer: Lowerer, + read: ts.ElementAccessExpression, + loc: SrcLoc, +): IrExpr | null { + if (lowerer.mapTypeOf(lowerer.typeOf(read.argumentExpression))?.kind !== "f64") return null; + const arr = lowerer.lowerExpr(read.expression); + if (arr.type.kind !== "array" || arr.type.elem.kind !== "string") return null; + const index = lowerer.lowerExprExpecting(read.argumentExpression, F64); + const stmts: IrStmt[] = []; + const held = (value: IrExpr, name: string): IrExpr => { + const tmp = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc }); + return varRef(tmp.id, value.type, loc); + }; + // An index that can assign can replace the array binding or field, so + // the array is held before it, as JavaScript reads it first. + const a = !mayAssign(index) && isSafeToRepeat(arr) ? arr : held(arr, "%elementArray"); + const i = isSafeToRepeat(index) ? index : held(index, "%elementIndex"); + const value: IrExpr = { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: { kind: "arrayState", arr: a, index: i, type: F64, loc }, + right: numLit(1, loc), + type: BOOL, + loc, + }, + then: { kind: "arrayGet", arr: a, index: i, type: STRING, loc }, + else_: strLit("undefined", loc), + type: STRING, + loc, + }; + return stmts.length === 0 ? value : { kind: "seqExpr", stmts, result: value, type: STRING, loc }; +} + +function isScalarArm(arm: IrType): boolean { + return arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm); +} + +/** A union of numbers, strings, booleans, null and undefined with at least + * one unit arm. */ +function scalarUnion(lowerer: Lowerer, unionId: string): boolean { + const arms = lowerer.unions.get(unionId)?.arms; + return !!arms?.some(isUnitType) && arms.every(isScalarArm); +} + +/** How the converting use of `x!` (see assertionOperand) reads it: + * "number" where the use applies ToNumber, "string" where it converts the + * value to a string (a template span, `+` with a string), "value" where the + * consumer lowers the union itself (equality, truthiness, a comparison with + * a string), null for other uses. */ +function convertingUseOf( + lowerer: Lowerer, + expr: ts.NonNullExpression, +): "number" | "string" | "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.isTemplateSpan(parent)) return parent.expression === outer ? "string" : null; if (ts.isTypeOfExpression(parent)) return "value"; if (ts.isPrefixUnaryExpression(parent)) { if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value"; @@ -1100,7 +1632,7 @@ function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" 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 (isString(other)) return ts.isNonNullExpression(partner) ? null : "string"; if (!numericPartner(lowerer.typeOf(other))) return "value"; return isString(expr) ? null : "number"; } @@ -1186,7 +1718,8 @@ export function assertedReadObservesUndefined( lowerer: Lowerer, node: ts.NonNullExpression, ): boolean { - if (convertingUseOf(lowerer, node) === "value") return true; + const use = convertingUseOf(lowerer, node); + if (use === "value" || use === "string") return true; let outer: ts.Expression = node; while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; const parent = outer.parent; @@ -1964,6 +2497,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 @@ -3136,11 +3686,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-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5c4278ded..920c61823 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 { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; @@ -182,6 +184,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, @@ -987,6 +1000,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)) { @@ -1068,13 +1087,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); @@ -2081,7 +2098,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 @@ -2733,6 +2758,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). @@ -2743,6 +2774,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 @@ -3059,6 +3104,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); @@ -3235,7 +3284,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 @@ -3247,15 +3308,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 @@ -3276,13 +3352,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 @@ -3301,13 +3371,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) { diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 13d1c29fc..4ea102769 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -676,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 diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 614d6a206..d3132d02f 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -804,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. @@ -841,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: @@ -929,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; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 84196476a..d43aaabf8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,6 @@ +import type { DiagnosticCode } from "../../diagnostics/codes.js"; +import { keepsInitializerLayout, notePreservedLayout } from "./structural-instances.js"; +import { jsGlobalValueByName } from "./global-values.js"; import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { allBytesArms, @@ -33,14 +36,15 @@ import { InternalCompilerError } from "../../errors.js"; * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; import { lowerOptionalIndex } from "./undefined-index.js"; -import { builtinNamespaceValue } from "./builtin-namespaces.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, @@ -238,11 +242,12 @@ import { globalSymbolKey } from "./expressions/global-symbols.js"; import { identifiersNamed } from "./identifier-uses.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { - builtinGlobalValue, + dereferencedElementWrite, jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, lowerStrictEquality, + nodeTypeError, } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; @@ -415,8 +420,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; @@ -750,57 +755,58 @@ 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); + // The first attempt's diagnostics: a statement that still fails + // after degrading reports them, since a retry does not repeat what + // lowered once (a poisoned generic instance) and its own diagnostics + // can miss the failure's cause. + let firstAttempt: ScrDiagnostic[] | null = null; 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++) { + const hits = new Set(); + try { + lowerer.withPartialHits(hits, () => + 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. + firstAttempt ??= lowerer.diags.slice(diagsBefore); + const giveUp = (): unknown => { + if (attempt > 0) lowerer.restoreFirstAttemptDiags(diagsBefore, firstAttempt!); + return e; + }; + if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw giveUp(); + const slot = lowerer.degradeAtCommit(e, STATEMENT_SLOTS); + // Pushed-down types the failed attempt mapped widen to the + // whole slot before anything else is given up. + if ( + slot === null && + !lowerer.reopenFlushedAtCommit(diagsBefore) && + !lowerer.widenPartials(hits) + ) + throw giveUp(); + // 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): @@ -813,7 +819,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 @@ -837,6 +853,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 @@ -862,6 +880,84 @@ 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), and the types of the statement's own expressions: casts, + * literals with a contextual type, spread clones, temporaries (stage L4). */ +const STATEMENT_SLOTS: ReadonlySet = new Set([ + "local", + "global", + "loop", + "param", + "return", + "field", + "member", + "expression", +]); + +/** 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 { @@ -1302,6 +1398,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) && @@ -1522,7 +1619,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 ( @@ -1533,12 +1634,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 @@ -1548,12 +1657,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). */ @@ -1683,8 +1793,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; if (pre) { - lowerer.ctx.tdzPredeclared.delete(symbol!); const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN); + lowerer.ctx.tdzPredeclared.delete(symbol!); return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) }; } const type: IrType = { kind: "classval", className: info.def.name }; @@ -1878,7 +1988,16 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt } // Exception-representable values move into the runtime's exception // cell. `throw f()` of a void call has no value. - let value = lowerer.lowerExpr(stmt.expression); + // 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 @@ -1949,7 +2068,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt const entry = UNSUPPORTED_STMT[stmt.kind]; if (entry) lowerer.unsupported( - entry.code as `SC${number}` & keyof typeof UNSUPPORTED, + entry.code as DiagnosticCode & keyof typeof UNSUPPORTED, stmt, entry.feature, ); @@ -3222,8 +3341,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; @@ -4326,14 +4445,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) } - : (builtinNamespaceValue(lowerer, canonical, 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)) { @@ -5700,8 +5814,8 @@ export function lowerVarDecl( const symbol = lowerer.checker.getSymbolAtLocation(decl.name); const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; if (pre) { - lowerer.ctx.tdzPredeclared.delete(symbol!); const value = genericBindingStoredValue(lowerer, decl, info); + lowerer.ctx.tdzPredeclared.delete(symbol!); return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) }; } return genericBindingPresenceStore(info, decl); @@ -5837,6 +5951,16 @@ export function lowerVarDecl( ) g.type = DYN; g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type); + // A module const keeps its initializer's record or array layout, as a + // function's const does (keepsInitializerLayout). + if ( + !isVarDeclared(decl) && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 && + keepsInitializerLayout(lowerer, decl.initializer, raw.type, g.type) + ) { + g.type = raw.type; + notePreservedLayout(lowerer, decl); + } const arithmetic = raw.type.kind === "union" ? lowerer.unions.get(raw.type.unionId)?.arms : undefined; if ( @@ -5864,11 +5988,13 @@ export function lowerVarDecl( // A forward-captured binding pre-declared as a TDZ box (an earlier // function in this scope captured it — predeclareForwardCapture): the // binding and its scope-entry varDecl already exist; the source - // declaration is the one initializing `assign` into the shared box. + // declaration is the one initializing `assign` into the shared box. The + // box stays pre-declared until the initializer lowers: a statement the + // commit point lowers again (localized failure) initializes the same box. const pre = declSymbol ? lowerer.ctx.tdzPredeclared.get(declSymbol) : undefined; if (pre && decl.initializer) { - lowerer.ctx.tdzPredeclared.delete(declSymbol!); const init = lowerer.lowerExprExpecting(decl.initializer, pre.type); + lowerer.ctx.tdzPredeclared.delete(declSymbol!); return { kind: "assign", localId: pre.id, value: init, initializes: true, loc: locOf(decl) }; } @@ -5984,6 +6110,17 @@ export function lowerVarDecl( // Promise.prototype is an ordinary object the checker types Promise. if (!decl.type && !hasJsTypeAnnotation(decl) && isPromisePrototypeRead(lowerer, decl.initializer)) type = DYN; + // A const whose initializer has another record or array layout than the + // binding's type keeps the initializer's layout: converting would copy the + // value, so the binding would not be the value its initializer names. + if ( + !isLet && + type !== null && + keepsInitializerLayout(lowerer, decl.initializer, init.type, type) + ) { + type = init.type; + notePreservedLayout(lowerer, decl); + } // A typed-arguments function expression (typed-arguments.ts): the // checker types the binding variadic, but its value has the fixed // signature every use asserts. @@ -6515,6 +6652,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. */ @@ -7822,6 +7994,29 @@ function lowerCjsSingleValueExport( * assignment, and `({ a, b } = e);` (destructuring ASSIGNMENT, which JS * requires parenthesized in statement position) lands in the * object-literal-target branch below. */ +/** dereferencedElementWrite in statement position. */ +function lowerDereferencedElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt | null { + const write = dereferencedElementWrite(lowerer, expr); + if (write === null) return null; + const loc = locOf(expr); + return { + kind: "block", + body: [ + ...write.stmts, + { + kind: "if", + cond: write.present, + then: [ + lowerer.withExpressionOverrides(write.overrides, () => lowerExprStatement(lowerer, expr)), + ], + else_: [{ kind: "exprStmt", expr: nodeTypeError(write.message, VOID, loc), loc }], + loc, + }, + ], + loc, + }; +} + export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStmt { const stmtNode = expr.parent; while (ts.isParenthesizedExpression(expr)) expr = expr.expression; @@ -8084,6 +8279,10 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const ex = lowerExpandoAssignStmt(lowerer, expr); if (ex) return ex; } + { + const dereferenced = lowerDereferencedElementWrite(lowerer, expr); + if (dereferenced) return dereferenced; + } if (ts.isElementAccessExpression(expr.left)) return lowerer.lowerElementWrite(expr); if (ts.isPropertyAccessExpression(expr.left)) { fenceNodeModuleMutation(lowerer, expr.left, "assignment"); @@ -8781,13 +8980,17 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm return { kind: "block", body: [], loc: locOf(expr) }; } if ( - stmtNode !== undefined && - ts.isExpressionStatement(stmtNode) && ts.isBinaryExpression(expr) && (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) ) { - return lowerDiscardedLogical(lowerer, expr); + // A right operand without a value (`skip || (flag ? warnA() : warnB())`, + // where both arms are void calls) leaves the logical nothing to join: + // it is the branch `if (!skip) ...` wherever its result is discarded, + // a for loop's initializer and update included. + if (needsNoValue(lowerer, expr.right)) return lowerLogicalBranch(lowerer, expr); + if (stmtNode !== undefined && ts.isExpressionStatement(stmtNode)) + return lowerDiscardedLogical(lowerer, expr); } const value = lowerer.lowerExpr(expr); // A bare unit-literal statement (`undefined;`): a pure no-op in JS, @@ -8830,6 +9033,32 @@ function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrS if (!local) throw e; lowerer.diags.length = diagsBefore; } + return lowerLogicalBranch(lowerer, expr); +} + +/** True when the discarded `expr` needs no value: its type maps to void, + * or it is a logical or comma expression whose last operand needs none. A + * discarded logical whose right operand needs no value lowers as a branch + * that runs the operand as a statement. */ +function needsNoValue(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if ( + ts.isBinaryExpression(expr) && + (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + expr.operatorToken.kind === ts.SyntaxKind.BarBarToken || + expr.operatorToken.kind === ts.SyntaxKind.CommaToken) + ) + return needsNoValue(lowerer, expr.right); + const type = lowerer.typeOf(expr); + return ( + (type.flags & (ts.TypeFlags.Void | ts.TypeFlags.TypeParameter)) !== 0 && + lowerer.mapTypeOf(type)?.kind === "void" + ); +} + +/** `a && b` as `if (a) b;` and `a || b` as `if (!a) b;`, with `b` lowered + * as a statement under the narrowing `a` gives it. */ +function lowerLogicalBranch(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt { const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; const cond = lowerer.lowerCondition(expr.left); if (cond.kind === "boolLit") { @@ -11624,6 +11853,7 @@ function lowerForOfDyn( const iterator = lowerer.declareHiddenLocal("%dofIterator", DYN); const next = lowerer.declareHiddenLocal("%dofNext", DYN); const fast = lowerer.declareHiddenLocal("%dofNative", BOOL); + const generator = lowerer.declareHiddenLocal("%dofGenerator", F64); const done = lowerer.declareHiddenLocal("%dofDone", BOOL); done.mutable = true; const value = lowerer.declareHiddenLocal("%dofValue", DYN); @@ -11633,6 +11863,15 @@ function lowerForOfDyn( needsClose.mutable = true; const error = lowerer.declareHiddenLocal("%dofError", CAUGHT); const syncIterator = asyncLoop ? lowerer.declareHiddenLocal("%dofSync", BOOL) : null; + lowerer.untypedLoops.set(generator, { + kind: generator.id, + value: value.id, + iterator: iterator.id, + next: next.id, + done: done.id, + async: asyncLoop, + sync: syncIterator?.id ?? null, + }); const awaitDyn = (value: IrExpr): IrExpr => ({ kind: "libCall", fn: "async.awaitDyn", @@ -11907,6 +12146,14 @@ function lowerForOfDyn( : iteratorCanStep(varRef(iterator.id, DYN, loc), varRef(next.id, DYN, loc), loc), loc, }, + // Which of the program's generator types the iterator is, once + // generator-dynamic-dispatch.ts knows them; 0 keeps the protocol. + { + kind: "varDecl", + localId: generator.id, + init: { kind: "numLit", value: 0, type: F64, loc }, + loc, + }, { kind: "varDecl", localId: done.id, @@ -12227,11 +12474,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 @@ -12241,10 +12485,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; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 7f1679855..c18b88a95 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,16 @@ +import type { DiagnosticCode } from "../../diagnostics/codes.js"; +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, @@ -8,9 +21,9 @@ import { import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js"; -import { keyedHelperName } from "./stable-names.js"; +import { keyedHelperName, moduleTagNumber, portableRelativePath } 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, @@ -168,6 +181,7 @@ import { } from "./class-dynamic-dispatch.js"; import { addUntypedIntegrityWords, guardClassAttributeWrites } from "./class-attributes.js"; import { inferredCollectionType } from "./js-collection-types.js"; +import { sloppyDynamicWrites } from "./frozen-records.js"; import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; import { installBufferValue } from "./containers/bytes.js"; @@ -175,7 +189,7 @@ import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js"; -import { dynamicTierWarning } from "./dynamic-tier.js"; +import { dynamicSlotOperationCounts, dynamicTierWarning } from "./dynamic-tier.js"; import { promoteJsvals } from "../../ir/jsval.js"; import { analyzeTiers, @@ -204,7 +218,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; * - Lexical scoping is resolved here: locals get function-unique ids * ("x.0", "x.1" for shadowing); the IR is scope-flat. */ -import { dirname, relative, resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; @@ -212,7 +226,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, @@ -284,12 +305,12 @@ import { cjsExportsAssignedInFunctions, cjsReexportedModules, cjsNamespaceExportNames, - fallbackDtsPath, isCheckedJsSourceFile, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isNodeEsmFile, + isFallbackDtsPath, isNodeTypesPath, locOf, orderedImportsOf, @@ -468,6 +489,7 @@ 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"; @@ -721,7 +743,45 @@ 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; + /** The slot keeps its type's compilable shape; only the failing + * components are checked-dynamic (push-down, stage L3). */ + partial?: true; +} + +/** 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 @@ -836,6 +896,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[]; @@ -907,7 +970,7 @@ export interface LowerOptions { } interface RuntimeFenceFallback { - code: `SC${number}`; + code: DiagnosticCode; message: string; } @@ -944,11 +1007,30 @@ type RuntimeFenceTarget = | RuntimeFenceFunctionTarget | RuntimeFenceClosureTarget; +/** A degraded checker type (Lowerer.degradedTypes): its lenient mapping and + * the failure that degraded it. */ +export interface DegradedType { + lenient: IrType | null; + code: string; + reason: string; +} + /** The Lowerer's pass configuration (see lowerToIr). */ 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; + /** Localized failure: degrade every type to the whole-slot checked + * dynamic, never to its pushed-down shape (lowerToIr's comparison + * pass). */ + wholeSlotDegradation?: boolean; /** LowerOptions.externalTypes, threaded through every lowering pass. */ externalTypes?: ReadonlyMap; /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ @@ -1174,6 +1256,39 @@ export function lowerToIr( let result = emitted.result; let resultLowerer = reachableEmit; timing("reachable-emit", { reachable: reachable.size }); + // Localized failure (stage L3): push-down must never cost a failing + // program more errors than the whole-slot checked dynamic. A pushed-down + // type is baked into the globals, fields and signatures that hold it, so + // when the program still fails, the emit runs once more with every + // degraded type held whole, and the pass with fewer errors wins (ties + // keep push-down). A program that compiles never takes this path. + if (result.diagnostics.length > 0 && reachableEmit.hasPartialDegradedTypes()) { + const whole = new Lowerer(program, entry, moduleOrder, { + frontendServices: options.frontendServices, + projectCheckJs: options.projectCheckJs ?? false, + targetPlatform, + startupCrash, + inlineRequireCycleCalls, + ffiImports, + libraryCallbacks, + forkTargets, + workerTargets, + externalTypes, + externalTypeSpecifiersByFile, + tierMode, + ffiBindingSymbols: ffiValidation.symbolsByName, + wholeSlotDegradation: true, + }); + for (const d of extraModuleCycleDiags) whole.pushDiag(d); + for (const d of ffiValidation.diagnostics) whole.pushDiag(d); + const wholeEmitted = whole.emitReachable(options.libRoots); + if (wholeEmitted.result.diagnostics.length < result.diagnostics.length) { + reachable = wholeEmitted.reachable; + result = wholeEmitted.result; + resultLowerer = whole; + } + timing("whole-slot-relower", { reachable: wholeEmitted.reachable.size }); + } // The general stream representation (TypeMapperCtx.generalNodeStreams) // is a per-program decision: only a program whose reachable code moved // a general stream into a stdio-represented stream slot lowers again @@ -1201,6 +1316,7 @@ export function lowerToIr( ffiBindingSymbols: ffiValidation.symbolsByName, generalNodeStreams: true, ...(dyn ? { dynNodeStreams: true } : {}), + ...(resultLowerer.wholeSlotDegradation ? { wholeSlotDegradation: true } : {}), }); for (const d of extraModuleCycleDiags) general.pushDiag(d); for (const d of ffiValidation.diagnostics) general.pushDiag(d); @@ -1244,6 +1360,7 @@ export function lowerToIr( externalTypes, externalTypeSpecifiersByFile, tierMode, + ...(resultLowerer.wholeSlotDegradation ? { wholeSlotDegradation: true } : {}), }); for (const d of extraModuleCycleDiags) emit.pushDiag(d); for (const d of ffiValidation.diagnostics) emit.pushDiag(d); @@ -1263,6 +1380,29 @@ 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 operations = dynamicSlotOperationCounts( + resultLowerer, + resultLowerer.dynamicBodies, + resultLowerer.dynamicSlots, + ); + const dynamicSlots: DynamicSlotEntry[] = resultLowerer.dynamicSlots.map((slot, i) => ({ + operations: operations[i]!, + loc: slot.loc, + name: slot.failure.name, + slot: slot.failure.slot, + declaredType: slot.failure.declaredType, + code: slot.code, + reason: slot.reason, + ...(slot.partial ? { partial: true } : {}), + })); + 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 @@ -1350,6 +1490,8 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + degradedTypes: resultLowerer.degradedTypes, + retriedSymbols: resultLowerer.retriedSymbols, inlineRequireCycleCalls, dynamicImportRejections: options.dynamicImportRejections, targetPlatform, @@ -1940,6 +2082,24 @@ function byName(fns: readonly IrFunction[]): IrFunction[] { ); } +/** A loop over an untyped iterable (lowerForOfDyn). `kind` is a local the + * loop sets once it has opened the iterator: 0 for the dynamic protocol, + * or the 1-based index of the program's generator type it steps natively. + * `value` is the per-step local whose initializer takes one step. */ +export interface UntypedLoopSite { + kind: string; + value: string; + iterator: string; + next: string; + done: string; + /** `for await`; `sync` names its async-from-sync flag. */ + async: boolean; + sync: string | null; +} + +/** The program module a createRequire binding requires. */ +type CreateRequireTarget = NonNullable>; + export class Lowerer { readonly frontendServices: FrontendServices | undefined; readonly checker: ts.TypeChecker; @@ -1961,6 +2121,15 @@ export class Lowerer { * exact binding discipline as globals/functions. */ readonly staticCallables = new Map(); readonly genericFnsBySymbol = new Map(); + /** Typed declarations' instances for arguments that keep their own + * record or array layout (structural-instances.ts). */ + readonly structuralFnInfos = new Map(); + /** Consts that keep their initializer's record or array layout, with the + * initializer's checker type (structural-instances.ts). */ + readonly preservedLayoutTypes = new Map(); + /** Arguments and const initializers whose array or record layout the + * lowering kept instead of copying (divergences.ts reports no copy there). */ + readonly layoutKeptNodes = new Set(); /** Object-literal GENERIC methods (`{ m(x: T) {...} }`), interned by * their function-like node — instances ride the same monomorphization * queue (objLitGenericFnInfoOf). */ @@ -2504,6 +2673,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 @@ -3152,6 +3384,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. */ @@ -3166,6 +3401,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< @@ -3353,10 +3590,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). */ @@ -3388,6 +3624,14 @@ export class Lowerer { * values other than fresh literals (noteUntypedIntegrity). */ untypedIntegrity = false; readonly liftedFns: IrFunction[] = []; + /** `for … of` and `for await` loops over untyped iterables, by their + * generator-kind local (generator-dynamic-dispatch.ts steps the program's + * own generators there on the typed path). Local ids repeat across + * functions; the local itself is in its function's locals. */ + readonly untypedLoops = new Map(); + /** 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(); @@ -3410,12 +3654,17 @@ export class Lowerer { * names do not depend on where the program sits, default library files * by their base name (two checkers install them in different * directories), and the files scriptc ships by their name inside scriptc, - * so names do not depend on where scriptc is installed either. */ - readonly positions = new StructuralPositions((sf) => - this.program.isSourceFileDefaultLibrary(sf) + * so names do not depend on where scriptc is installed either. Relative + * paths use `/` on every platform. */ + readonly positions = new StructuralPositions((sf) => this.stableFileName(sf)); + + /** The name of `sf` in positions and module tags. */ + stableFileName(sf: ts.SourceFile): string { + return this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)), - ); + : (compilerOwnedFileName(sf.fileName) ?? + portableRelativePath(dirname(this.entry.fileName), sf.fileName)); + } /** The outermost node whose token span is exactly `loc`, or null. */ nodeAtLoc(loc: SrcLoc): ts.Node | null { @@ -3726,6 +3975,64 @@ 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(); + /** A pushed-down (partial) degraded type was mapped (hasPartialDegradedTypes). */ + private partialDegradedTypesMapped = false; + /** The components lenient mappings turned into the checked dynamic, by + * checker type id, with the declared type they are pieces of (SC6101 + * causes and per-slot operation counts). */ + readonly lenientPieces = new Map(); + /** One frame per unit a commit point may retry: the pushed-down + * (partial) degraded types its lowering mapped (withPartialHits). */ + readonly partialHits: Set[] = []; + /** 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; + /** LowererMode.wholeSlotDegradation. */ + readonly wholeSlotDegradation: boolean; + /** 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). */ @@ -3779,12 +4086,12 @@ 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 @@ -3878,6 +4185,16 @@ export class Lowerer { ) { // The mode's options, sorted by name; the values derived from them follow. this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); + // Lenient forms are IR types of the other pass's shape tables: only the + // whole-slot dynamic carries over as is. + if (mode.degradedTypes) + for (const [id, degraded] of mode.degradedTypes) + this.degradedTypes.set(id, { + ...degraded, + lenient: degraded.lenient?.kind === "dyn" ? DYN : null, + }); + this.priorRetriedSymbols = mode.retriedSymbols ?? new Set(); + this.wholeSlotDegradation = mode.wholeSlotDegradation ?? false; this.externalTypes = mode.externalTypes ?? new Map(); this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; this.ffiImports = mode.ffiImports ?? []; @@ -3905,6 +4222,7 @@ 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), @@ -3914,6 +4232,25 @@ export class Lowerer { resolveTypeParam: this.typeParamResolver, resolveTypeParamTs: this.typeParamTsResolver, resolveBoundAliasTs: this.boundAliasTsResolver, + degradedType: (type) => { + const id = (type as { id?: number }).id; + if (id === undefined) return null; + const degraded = this.degradedTypes.get(id); + if (degraded === undefined) return null; + // An entry handed over from another pass (the coverage remainder) + // re-derives its lenient form in this lowerer's shape tables; the + // checked dynamic cuts the knot while it does. + if (degraded.lenient === null) { + degraded.lenient = DYN; + degraded.lenient = this.lenientType(type); + } + if (degraded.lenient.kind !== "dyn") { + this.partialHits[this.partialHits.length - 1]?.add(id); + this.partialDegradedTypesMapped = true; + } + return degraded.lenient; + }, + ...(shadow !== undefined ? { canonShadow: shadow } : {}), isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), @@ -4027,9 +4364,20 @@ export class Lowerer { // moduleOrder BEFORE any pass constructed — lowerToIr runs // appendDynamicImportModules once on the shared array (a per-pass run // here would repeatedly extend the graph and duplicate cycle reports). - this.moduleOrder.forEach((sf, i) => { - this.fileTag.set(sf, sf === entry ? "" : `%m${i}.`); - }); + // A module's tag hashes its stable file name rather than numbering its + // place in moduleOrder, so adding or reordering modules leaves the other + // modules' function names, and the closure names keyed by them, + // unchanged. The native compiler's profile matches functions by name. + const tagNumbers = new Set(); + for (const sf of this.moduleOrder) { + if (sf === entry) { + this.fileTag.set(sf, ""); + continue; + } + const n = moduleTagNumber(this.stableFileName(sf), tagNumbers); + tagNumbers.add(n); + this.fileTag.set(sf, `%m${n}.`); + } if (this.moduleOrder.length === 0) this.fileTag.set(entry, ""); const jsDynamic = jsDynamicDeclarations(this.moduleOrder, this.checker); if (jsDynamic.size > 0) this.typeCtx.jsDynamicDeclaration = (decl) => jsDynamic.has(decl); @@ -4764,6 +5112,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 @@ -4771,9 +5168,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); @@ -4818,18 +5213,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)) @@ -4946,7 +5332,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 @@ -5939,6 +6332,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 @@ -8204,9 +8613,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) { @@ -8215,7 +8627,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); + } } } @@ -8250,15 +8665,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++]!)); } @@ -8271,13 +8709,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 @@ -8431,6 +8866,8 @@ export class Lowerer { // reference are untouched — byte-stability holds. checkRequireBindingAccesses(this, functions); refuseX509DynValues(this, functions); + markInferredFactChecks(functions, this.program); + const sloppyDynWrites = sloppyDynamicWrites(this, functions); const module: IrModule | null = this.diags.length > 0 ? null @@ -8446,6 +8883,13 @@ export class Lowerer { ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), ...(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() } : {}), @@ -8457,6 +8901,7 @@ export class Lowerer { ? { freezableArrays: [...this.freezableArrays].sort() } : {}), ...(this.untypedIntegrity ? { untypedIntegrity: true as const } : {}), + ...(sloppyDynWrites.length > 0 ? { sloppyDynWrites } : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; @@ -8540,6 +8985,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; @@ -8718,6 +9179,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++, @@ -8846,6 +9321,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 @@ -8863,6 +9342,44 @@ export class Lowerer { register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop)); } }; + // 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 @@ -8918,17 +9435,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; } } } @@ -9300,7 +9816,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. @@ -9320,7 +9843,7 @@ export class Lowerer { } unsupported( - code: keyof typeof UNSUPPORTED & `SC${number}`, + code: keyof typeof UNSUPPORTED & DiagnosticCode, node: ts.Node, featureOverride?: string, hintOverride?: string, @@ -9355,6 +9878,268 @@ 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 to the whole-slot dynamic and still failing: no + // progress, commit as today. + if (id === undefined || this.degradedTypes.get(id)?.lenient?.kind === "dyn") 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.degradeType(type, committed.code, committed.message); + const partial = this.degradedTypes.get(id)?.lenient?.kind !== "dyn"; + return { + ...(partial ? { partial: true as const } : {}), + 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); + // A pushed-down type is one slot, reported where it degraded; only + // whole-slot dynamic declarations are reported one by one. + if (degraded === undefined || degraded.lenient?.kind !== "dyn") 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); + } + + /** A unit that still fails after its retries commits its first + * attempt's own diagnostics (`first`, taken before the first + * degradation) in place of the last attempt's. The diagnostics the + * retries carry (dropAttemptDiags) stay as they are now: a declaration + * reopened since may have registered. */ + restoreFirstAttemptDiags(diagsBefore: number, first: readonly ScrDiagnostic[]): void { + const carried = (d: ScrDiagnostic): boolean => + d.rejects !== undefined || this.loweredOnceDiags.has(d); + const kept = this.diags.slice(diagsBefore).filter(carried); + this.diags.length = diagsBefore; + this.diags.push(...first.filter((d) => !carried(d)), ...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; + const hits = new Set(); + try { + const result = this.withPartialHits(hits, 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 && !this.widenPartials(hits)) return null; + if (slot !== null) slots.push(slot); + this.dropAttemptDiags(mark); + } + } + } + + /** Localized failure's lenient mapping (stage L3, push-down): the type + * mapped with each failing component as the checked dynamic, so a record + * keeps its static members, an array of records its records, and a + * function its arity. The whole slot is the checked dynamic when even + * that shape does not map. Throwaway context: nothing memoizes. */ + lenientType(type: ts.Type): IrType { + const { typeMemo: _strictMemo, ...strict } = this.typeCtx; + const owner = this.checker.typeToString(type); + const mapped = mapType(type, { + ...strict, + lenient: true, + canMemoizeType: () => false, + onLenientPiece: (piece) => { + const id = (piece as { id?: number }).id; + if (id !== undefined && !this.lenientPieces.has(id)) this.lenientPieces.set(id, owner); + }, + }); + return mapped === null || mapped.kind === "void" ? DYN : mapped; + } + + /** Degrades a type for a commit point: its lenient (pushed-down) form + * first; a type already degraded that still fails falls back to the + * whole-slot checked dynamic. False when nothing is left to degrade. */ + private degradeType(type: ts.Type, code: string, reason: string): boolean { + const id = (type as { id?: number }).id; + if (id === undefined) return false; + const known = this.degradedTypes.get(id); + if (known !== undefined) { + if (known.lenient?.kind === "dyn") return false; + this.degradedTypes.set(id, { lenient: DYN, code, reason }); + return true; + } + this.degradedTypes.set(id, { + lenient: this.wholeSlotDegradation ? DYN : this.lenientType(type), + code, + reason, + }); + return true; + } + + /** Some degraded type kept a pushed-down (partial) shape, or one was + * mapped before a retry widened it: the IR lowered before the retry keeps + * that shape. */ + hasPartialDegradedTypes(): boolean { + if (this.partialDegradedTypesMapped) return true; + for (const degraded of this.degradedTypes.values()) + if (degraded.lenient !== null && degraded.lenient.kind !== "dyn") return true; + return false; + } + + /** Runs a unit a commit point may retry, recording the pushed-down + * types it maps; the frame's hits also count for the enclosing unit. */ + withPartialHits(hits: Set, run: () => T): T { + this.partialHits.push(hits); + try { + return run(); + } finally { + this.partialHits.pop(); + const outer = this.partialHits[this.partialHits.length - 1]; + if (outer !== undefined) for (const id of hits) outer.add(id); + } + } + + /** Push-down is a refinement, never a new failure: a unit that still + * fails after mapping pushed-down types falls back to the whole-slot + * checked dynamic for them. True when some type widened (retry). */ + widenPartials(hits: ReadonlySet): boolean { + if (this.tierMode !== "auto" || this.diagSink !== null) return false; + let widened = false; + for (const id of hits) { + const degraded = this.degradedTypes.get(id); + if (degraded === undefined || degraded.lenient?.kind === "dyn") continue; + degraded.lenient = DYN; + widened = true; + } + return widened; + } + + /** 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, @@ -9366,8 +10151,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 @@ -9378,10 +10167,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): @@ -9390,14 +10181,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 @@ -9427,19 +10220,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, @@ -9455,8 +10255,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 @@ -9476,16 +10280,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 @@ -9497,8 +10309,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 @@ -9528,26 +10344,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 @@ -9981,6 +10807,38 @@ export class Lowerer { ); }; + 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 @@ -9995,6 +10853,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; @@ -10256,6 +11115,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) { @@ -11120,6 +11991,26 @@ export class Lowerer { 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 @@ -11282,6 +12173,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; } @@ -11605,6 +12515,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); @@ -12198,6 +13109,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; @@ -12214,20 +13126,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 @@ -12244,6 +13184,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); @@ -12251,9 +13192,163 @@ 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) and their entries before it, for retryDeferredAtDeclaration to + // restore when it fails. + const degraded: { id: number; prior: DegradedType | undefined }[] = []; + 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: { id: number; prior: DegradedType | undefined }[], + ): boolean { + let current = diags; + for (let round = 0; round < MAX_DECLARATION_RETRIES; round++) { + const failing: { diag: ScrDiagnostic; failure: TypeFailure; type: ts.Type }[] = []; + 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 || + type === undefined || + id === undefined || + !failure.degradable || + !DECLARATION_SLOTS.has(failure.slot) || + this.degradedTypes.get(id)?.lenient?.kind === "dyn" + ) + return false; + failing.push({ diag, failure, type }); + } + // 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, type } of failing) { + const id = (type as { id?: number }).id ?? -1; + const prior = this.degradedTypes.get(id); + degraded.push({ + id, + prior: + prior === undefined + ? undefined + : { lenient: prior.lenient, code: prior.code, reason: prior.reason }, + }); + this.degradeType(type, diag.code, diag.message); + const partial = this.degradedTypes.get(id)?.lenient?.kind !== "dyn"; + slots.push({ + loc: diag.loc, + failure, + code: diag.code, + reason: diag.message, + ...(partial ? { partial: true as const } : {}), + }); + } + this.deferredDiags.delete(symbol); + const lifted = this.liftDynFieldPolicy; + this.liftDynFieldPolicy = true; + // Pushed-down types that did not help widen to the whole slot, and the + // collection runs again (a few times at most). + let registered = false; + try { + for (let widen = 0; widen < 4; widen++) { + const hits = new Set(); + registered = + this.withPartialHits(hits, () => + this.collectDeferring(collector.symbolOf, collector.collect), + ) === null; + if (registered || !this.widenPartials(hits)) break; + this.deferredDiags.delete(symbol); + } + } 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: { id: number; prior: DegradedType | undefined }[] = []; + + /** 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 (let i = this.retryDegraded.length - 1; i >= 0; i--) { + const { id, prior } = this.retryDegraded[i]!; + if (prior === undefined) this.degradedTypes.delete(id); + else this.degradedTypes.set(id, prior); + } + this.deferredDiags.set(symbol, [...diags]); + } + this.retryDegraded = []; + } + this.staticInitsPassed.add(symbol); + return registered; } collectSignature(decl: ts.FunctionDeclaration): void { @@ -12532,6 +13627,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 @@ -12710,6 +13808,7 @@ export class Lowerer { /* ── scoping and captures ─────────────────────────────────────────── */ declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal { + if (this.degradedTypes.size > 0) this.noteDegradedSlot(nameNode, this.typeOf(nameNode)); const symbol = this.checker.getSymbolAtLocation(nameNode); const local = declareContextLocal( this.ctx, @@ -12787,7 +13886,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) @@ -13369,8 +14468,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 { @@ -13644,7 +14743,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)); @@ -14056,7 +15155,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 { @@ -14351,6 +15452,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; @@ -14648,11 +15761,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); diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 24c981bd3..9456cfa18 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -451,6 +451,19 @@ export class NanCodedReads { // ---- present reads ------------------------------------------------------ + /** `xs[i]` whose index is a non-negative integer counter of the reading + * function and that a dominating `i < xs.length` guard bounds by the + * length: the read names an existing index. */ + counterIndexInBounds(read: ts.ElementAccessExpression): boolean { + const index = peelTransparent(read.argumentExpression); + if (!ts.isIdentifier(index)) return false; + const symbol = this.symbolOf(index); + if (!symbol || !this.nonNegativeCounter(symbol)) return false; + const decl = this.host.declarationOf(symbol); + if (!decl || this.containingFunction(decl) !== this.containingFunction(read)) return false; + return indexReadInBounds(read); + } + /** `t[i]` names an existing element: `i` is a non-negative integer * counter and a dominating guard bounds it by `t.length` (or by the * length of a typed array that always has the same length). diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index d0c9b04a6..9fae65a89 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -1,4 +1,5 @@ import * as ts from "../ts7/adapter.js"; +import { heritageElementTarget } from "../heritage-elements.js"; import { InternalCompilerError } from "../../errors.js"; import { typeEquals } from "../../ir/ir.js"; import type { IrType } from "../../ir/ir.js"; @@ -674,8 +675,9 @@ export class NarrowingStability { if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true; for (const clause of node.heritageClauses?.nodes ?? []) { for (const heritage of clause.types.nodes) { - const e = heritage.expression; - if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return true; + const e = heritageElementTarget(heritage); + if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e) && !ts.isQualifiedName(e)) + return true; } } return node.members.nodes.some( diff --git a/packages/compiler/src/frontend/lowering/stable-names.test.ts b/packages/compiler/src/frontend/lowering/stable-names.test.ts new file mode 100644 index 000000000..dbef9b29e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/stable-names.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "vitest"; +import { moduleTagNumber, portableRelativePath } from "./stable-names.js"; + +test("module tag numbers depend only on the file name and stay distinct", () => { + const first = moduleTagNumber("lib/a.ts", new Set()); + expect(moduleTagNumber("lib/a.ts", new Set([7, 8]))).toBe(first); + expect(moduleTagNumber("lib/b.ts", new Set())).not.toBe(first); + expect(moduleTagNumber("lib/a.ts", new Set([first]))).toBe((first + 1) >>> 0); + expect(moduleTagNumber("lib/a.ts", new Set([first, (first + 1) >>> 0]))).toBe((first + 2) >>> 0); +}); + +test("relative module paths use / on every platform", () => { + expect(portableRelativePath("/repo/src", "/repo/src/lib/a.ts", "linux")).toBe("lib/a.ts"); + expect(portableRelativePath("C:/repo/src", "C:/repo/src/lib/a.ts", "win32")).toBe("lib/a.ts"); + expect(portableRelativePath("C:\\repo\\src", "C:/repo/node_modules/x/index.d.ts", "win32")).toBe( + "../node_modules/x/index.d.ts", + ); + expect( + moduleTagNumber(portableRelativePath("C:/repo", "C:/repo/lib/a.ts", "win32"), new Set()), + ).toBe(moduleTagNumber(portableRelativePath("/repo", "/repo/lib/a.ts", "linux"), new Set())); +}); diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts index 8f7d0967e..0d57bd2c9 100644 --- a/packages/compiler/src/frontend/lowering/stable-names.ts +++ b/packages/compiler/src/frontend/lowering/stable-names.ts @@ -1,4 +1,7 @@ +import * as posix from "node:path/posix"; +import * as win32 from "node:path/win32"; import { InternalCompilerError } from "../../errors.js"; +import { tsgoPath } from "../ts7/session-path.js"; /** The owner of the names keyedHelperName hands out. */ export interface KeyedNameOwner { @@ -46,3 +49,26 @@ export function keyedHelperName( throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); return name; } + +/** The number in the `%m.` tag of the module named `fileName`: a + * 32-bit FNV-1a hash of the name, so a module keeps its tag when other + * modules are added, removed or reordered. A number in `taken` moves to the + * next free one, which keeps tags distinct. */ +export function moduleTagNumber(fileName: string, taken: ReadonlySet): number { + let h = 0x811c9dc5; + for (let i = 0; i < fileName.length; i++) h = Math.imul(h ^ fileName.charCodeAt(i), 0x01000193); + let n = h >>> 0; + while (taken.has(n)) n = (n + 1) >>> 0; + return n; +} + +/** The path of `file` relative to the directory `from`, with `/` separators + * on every platform, so names built from it match across hosts. */ +export function portableRelativePath( + from: string, + file: string, + platform: NodeJS.Platform = process.platform, +): string { + const path = platform === "win32" ? win32.relative(from, file) : posix.relative(from, file); + return tsgoPath(path, platform); +} 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/structural-instances.ts b/packages/compiler/src/frontend/lowering/structural-instances.ts new file mode 100644 index 000000000..c321bacd7 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-instances.ts @@ -0,0 +1,295 @@ +import * as ts from "../ts7/adapter.js"; +import type { IrType } from "../../ir/ir.js"; +import { typeEquals, UNDEFINED_T } from "../../ir/ir.js"; +import type { FnSig, ParamShape } from "./call-signatures.js"; +import { isThisParameter } from "./call-signatures.js"; +import { + internImplicitInstanceOr, + paramWrittenInBody, + type GenericFnInfo, + type GenericInstance, +} from "./generic-functions.js"; +import { nsPathPrefix } from "./lower-namespaces.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Instances of a typed function for arguments that keep their own layout. + * + * A record or array whose layout differs from a parameter's declared + * structural type converts by copying it (`{ id; name; owned?: true }[]` + * into `{ id; name }[]`). JavaScript passes the same object, so the + * callee's writes (`push`, index stores, element-field writes) and identity + * comparisons see the caller's value. When an argument names an existing + * value, the call instead targets an instance of the function lowered with + * that parameter bound to the argument's own type: the body reads and + * writes the caller's storage. Instances are keyed by the parameter + * layouts, so each layout combination compiles once. */ + +/** The argument expressions that can name a value another reference sees: + * bindings, member reads and `this`. Fresh values (literals, calls) have no + * other reference, so their copy is not observable. */ +function namesExistingValue(arg: ts.Expression): boolean { + let e = arg; + for (;;) { + if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression; + else if (ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) e = e.expression; + else break; + } + return ( + ts.isIdentifier(e) || + ts.isPropertyAccessExpression(e) || + ts.isElementAccessExpression(e) || + e.kind === ts.SyntaxKind.ThisKeyword + ); +} + +/** Whether a value of layout `from` can stand for a value of layout `to` + * in code written against `to`: the same layout, or a record that has every + * field of `to` at such a layout plus fields `to` lacks that are optional + * (`{ id; name; owned?: true }` for `{ id; name }`), or an array of such + * elements. Every value that code stores converts into `from` by completing + * the optional fields as absent, so reads and writes through either type see + * one value. */ +function standsFor( + lowerer: Lowerer, + from: IrType, + to: IrType, + assumed: Set = new Set(), +): boolean { + if (typeEquals(from, to)) return true; + if (from.kind === "array" && to.kind === "array") + return standsFor(lowerer, from.elem, to.elem, assumed); + if (from.kind !== "record" || to.kind !== "record") return false; + // Recursive layouts (`{ kids: Tree[] }` against `{ kids: Plain[] }`) meet + // the same pair again inside their fields; the pair holds there if it + // holds for the other fields. + const pair = `${from.shapeId}>${to.shapeId}`; + if (assumed.has(pair)) return true; + assumed.add(pair); + const source = lowerer.shapes.get(from.shapeId); + const target = lowerer.shapes.get(to.shapeId); + if (!source || !target || source.tuple || target.tuple) return false; + if (source.indexValue || target.indexValue) return false; + for (const field of target.fields) { + const kept = source.fields.find((f) => f.name === field.name); + if (!kept || !standsFor(lowerer, kept.type, field.type, assumed)) return false; + } + return source.fields.every( + (field) => + target.fields.some((f) => f.name === field.name) || + (field.type.kind === "union" && lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0), + ); +} + +/** Whether converting `from` into `to` copies a value that could keep its + * own layout instead. */ +function copies(lowerer: Lowerer, from: IrType, to: IrType): boolean { + if (typeEquals(from, to) || !standsFor(lowerer, from, to)) return false; + const plan = lowerer.widthLiftPlan(from, to); + return plan !== null && (plan.how === "arr" || plan.how === "width"); +} + +/** The structural-instance info of a typed function declaration, created on + * first use and kept on the Lowerer. */ +function structuralInfoOf( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + symbol: ts.Symbol, +): GenericFnInfo { + const existing = lowerer.structuralFnInfos.get(symbol); + if (existing) return existing; + const nameText = decl.name?.text ?? "%default"; + const body = decl.body!; + const info: GenericFnInfo = { + decl, + baseName: nameText, + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText + "%layout", + ), + typeParams: [], + instances: new Map(), + implicitParams: decl.parameters.nodes.map((param) => { + if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return null; + if (param.questionToken) return null; + const sym = lowerer.checker.getSymbolAtLocation(param.name); + if (!sym || paramWrittenInBody(lowerer, body, sym, param.name.text)) return null; + return sym; + }), + }; + lowerer.structuralFnInfos.set(symbol, info); + return info; +} + +/** The instance a direct call of the declared function `sig` targets when + * one of its arguments names a record or array whose layout the parameter + * would copy; null when every argument converts as declared. */ +export function structuralCallInstance( + lowerer: Lowerer, + expr: ts.CallExpression, + sig: FnSig, +): GenericInstance | null { + const symbol = lowerer.resolveValueSymbol(expr.expression as ts.Identifier); + const decl = symbol?.valueDeclaration; + if (!symbol || !decl || !ts.isFunctionDeclaration(decl) || !decl.body) return null; + if ( + decl.asteriskToken || + decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) + ) + return null; + if (decl.parameters.nodes.some(isThisParameter)) return null; + if (symbol.declarations.filter((d) => ts.isFunctionDeclaration(d)).length !== 1) return null; + const args = expr.arguments.nodes; + if (args.some((a) => ts.isSpreadElement(a))) return null; + const params = decl.parameters.nodes; + if (sig.params.length !== params.length) return null; + const bound: { index: number; type: IrType; checker: ts.Type }[] = []; + params.forEach((_, index) => { + const arg = args[index]; + const declared = sig.params[index]!; + if (!arg || declared.mode !== "required" || !namesExistingValue(arg)) return; + // A signature that widens the declared type's layout (record parameters + // whose fields also take undefined) keeps its own boundary conversion. + const own = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(params[index]!.name)); + if (!own || !typeEquals(own, declared.type)) return; + const checker = preservedLayoutOf(lowerer, arg) ?? lowerer.typeOf(arg); + const type = lowerer.mapTypeOf(checker); + if (type && copies(lowerer, type, declared.type)) bound.push({ index, type, checker }); + }); + if (bound.length === 0) return null; + const info = structuralInfoOf(lowerer, decl, symbol); + const shapes: ParamShape[] = [...sig.params]; + const argTypes = new Map(); + for (const { index, type, checker } of bound) { + const sym = info.implicitParams![index]; + if (!sym) continue; + shapes[index] = { type, mode: "required" }; + argTypes.set(sym, checker); + } + if (argTypes.size === 0) return null; + // A body that returns a bound parameter returns the argument's layout. + const previous = lowerer.implicitParamTypes; + lowerer.implicitParamTypes = argTypes; + let returnType: IrType; + try { + returnType = layoutReturnType(lowerer, decl, sig.returnType); + } finally { + lowerer.implicitParamTypes = previous; + } + const instance = internImplicitInstanceOr( + lowerer, + expr, + info, + shapes, + argTypes, + "", + returnType, + true, + ); + if (instance) for (const { index } of bound) lowerer.layoutKeptNodes.add(args[index]!); + return instance; +} + +/** The layout of the value an existing-value expression names: a binding's + * own type, or the element or field of such a value read through `[i]` or + * `.name`. Reads of a parameter bound to an argument's layout follow that + * layout rather than the parameter's declared type. */ +function layoutOf(lowerer: Lowerer, expression: ts.Expression): IrType | null { + let e = expression; + while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression; + if (ts.isElementAccessExpression(e)) { + const base = layoutOf(lowerer, e.expression); + if ( + base?.kind === "array" && + lowerer.mapTypeOf(lowerer.typeOf(e.argumentExpression))?.kind === "f64" + ) + return base.elem; + } else if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) { + const base = layoutOf(lowerer, e.expression); + if (base?.kind === "record") { + const field = lowerer.shapes.get(base.shapeId)?.fields.find((f) => f.name === e.name.text); + if (field) return field.type; + } + } + return lowerer.mapTypeOf(lowerer.typeOf(e)); +} + +/** The return type of a declared function whose every `return` names an + * existing value of one record or array layout that the declared return type + * would copy (`function items(values: Refined[]): Value[] { return values; }`): + * that layout, so the caller receives the value itself. `declared` otherwise. */ +export function layoutReturnType( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + declared: IrType, +): IrType { + if (!decl.body || (declared.kind !== "array" && declared.kind !== "record")) return declared; + // An exported function keeps its declared result: a library build calls it + // from outside the program, through the declared ABI. + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return declared; + const returned: ts.Expression[] = []; + let open = false; + ts.walkPreorder(decl.body, (node) => { + if (node !== decl.body && ts.isFunctionLike(node)) return "skip"; + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return "skip"; + if (!ts.isReturnStatement(node)) return undefined; + if (!node.expression || !namesExistingValue(node.expression)) open = true; + else returned.push(node.expression); + return undefined; + }); + if (open || returned.length === 0) return declared; + let layout: IrType | null = null; + for (const expression of returned) { + const type = layoutOf(lowerer, expression); + if (!type || !copies(lowerer, type, declared)) return declared; + if (layout && !typeEquals(layout, type)) return declared; + layout = type; + } + return layout ?? declared; +} + +/** The initializer type of a const the argument names when the const kept + * that initializer's layout (keepsInitializerLayout): the checker types the + * binding by its annotation, but its value has the initializer's layout. A + * declaration lowers once per function instance, and only some instances + * keep a layout, so the recorded type applies only while it maps to the + * layout of the binding the current lowering resolves. */ +function preservedLayoutOf(lowerer: Lowerer, arg: ts.Expression): ts.Type | undefined { + let e = arg; + while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression; + if (!ts.isIdentifier(e)) return undefined; + const symbol = lowerer.resolveValueSymbol(e); + const recorded = symbol ? lowerer.preservedLayoutTypes.get(symbol) : undefined; + if (recorded === undefined) return undefined; + const binding = lowerer.peekLocal(e)?.type ?? lowerer.globalOf(e)?.type; + const layout = lowerer.mapTypeOf(recorded); + return binding && layout && typeEquals(binding, layout) ? recorded : undefined; +} + +/** Whether a `const` binding typed `declared` keeps the record or array + * layout `actual` of an initializer that names an existing value or returns + * one, instead of converting it by a copy. Fresh literals convert as + * declared: their copy is not observable. */ +export function keepsInitializerLayout( + lowerer: Lowerer, + initializer: ts.Expression, + actual: IrType, + declared: IrType, +): boolean { + let e = initializer; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!namesExistingValue(e) && !ts.isCallExpression(e)) return false; + if (lowerer.isRuntimeOptionalRecordVariant(actual, declared)) return false; + return copies(lowerer, actual, declared); +} + +/** Records that the const `decl` keeps its initializer's layout, so calls + * that pass it bind the initializer's type (preservedLayoutOf). */ +export function notePreservedLayout(lowerer: Lowerer, decl: ts.VariableDeclaration): void { + if (!ts.isIdentifier(decl.name) || !decl.initializer) return; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (!symbol) return; + const initializer = + preservedLayoutOf(lowerer, decl.initializer) ?? lowerer.typeOf(decl.initializer); + lowerer.preservedLayoutTypes.set(symbol, initializer); + lowerer.layoutKeptNodes.add(decl.initializer); +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cf0428b60..3520e3aea 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2223,8 +2223,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< 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. 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..31004ca4f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/type-failure.ts @@ -0,0 +1,199 @@ +/* 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; + } + // A type node names its expression (`raw as Plugin`, not `Plugin`). + let named: ts.Node = node; + while (ts.isTypeNode(named) && named.parent !== undefined && !ts.isSourceFile(named.parent)) + named = named.parent; + const text = ts.getTextOfNode(named).replace(/\s+/g, " "); + 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 e415add7c..e837d59e6 100644 --- a/packages/compiler/src/frontend/lowering/typed-arguments.ts +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -195,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 @@ -262,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/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 5493b108d..34d8461dc 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -205,14 +205,26 @@ 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; +} + +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 diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 39dc27e3d..1728a4528 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"; @@ -804,8 +805,11 @@ 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, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index abe936c28..099e020f3 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -105,6 +105,8 @@ import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import { ambientDtsPath, fallbackDtsPath, + fallbackModulesNamedIn, + isFallbackDtsPath, isNodeTypesPath, overridesDtsPath, tsgoPath, @@ -147,7 +149,12 @@ import { } from "./decl-projection.js"; import { configureTypePatches, packageSourceText } from "./type-patches/layer.js"; import { resetDeclOriginVersions } from "./lowering/decl-origin.js"; -import { reportShapes } from "./shape-inference/index.js"; +import { + reportShapes, + resetShapeOverlay, + shapeOverlayEnabled, + shapeOverlayer, +} from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -626,6 +633,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 @@ -869,6 +897,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"]; @@ -910,12 +941,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()), @@ -1010,6 +1052,26 @@ function loadProgram7( } } } + // 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); @@ -1277,6 +1339,7 @@ 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"]; @@ -5066,7 +5129,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); }); } @@ -7198,7 +7261,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, diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index c403c6ba8..2a0ee09a1 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -70,6 +70,8 @@ export interface LiteralSite { 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; @@ -129,6 +131,17 @@ export interface ShapeAnalysis { 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. */ @@ -238,6 +251,47 @@ function isLoop(node: Node): boolean { ); } +/** 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", @@ -357,6 +411,11 @@ class Analyzer { 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, }; } @@ -449,6 +508,78 @@ class Analyzer { 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 })); @@ -717,6 +848,24 @@ class Analyzer { 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}` : ""; } @@ -812,8 +961,11 @@ class Analyzer { } const body = fn.body; if (body !== undefined) { - if (ts.isBlock(body)) this.stmts(list(body.statements)); - else this.edge(this.expr(body as ts.Expression), info.ret); + 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; @@ -1147,8 +1299,9 @@ class Analyzer { return; } if (ts.isReturnStatement(s)) { - if (s.expression !== undefined && this.currentFn !== null) - this.edge(this.expr(s.expression), this.currentFn.ret); + 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)) { @@ -1222,6 +1375,10 @@ class Analyzer { 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); @@ -1316,8 +1473,9 @@ class Analyzer { 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.prim("boolean")); + return this.constNode(this.boolLiteral(false)); case ts.SyntaxKind.NullKeyword: return this.constNode(this.prim("null")); case ts.SyntaxKind.RegularExpressionLiteral: @@ -1749,6 +1907,7 @@ class Analyzer { node: e, kind: "object", keys: keysKnown ? keys : null, + ownKeys: keys, hasAccessor, hasProto, hasComputedKey, @@ -1915,6 +2074,7 @@ class Analyzer { node: e, kind: "array", keys: null, + ownKeys: [], hasAccessor: false, hasProto: hasHole, hasComputedKey: false, @@ -2558,7 +2718,11 @@ class Analyzer { this.activate(info); this.add(info.thisNode, inst); this.bindArgs(info, args, spread); - this.edge(info.ret, dst); + // `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; @@ -3205,6 +3369,21 @@ class Analyzer { 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; } } @@ -3227,3 +3406,34 @@ function cjsExportName(decl: Node): string | null { 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/domain.ts b/packages/compiler/src/frontend/shape-inference/domain.ts index d90a83cf2..21e52346c 100644 --- a/packages/compiler/src/frontend/shape-inference/domain.ts +++ b/packages/compiler/src/frontend/shape-inference/domain.ts @@ -38,6 +38,8 @@ export interface Atom { /** 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 { diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts index 402a788b1..e2f7add04 100644 --- a/packages/compiler/src/frontend/shape-inference/index.ts +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -1,7 +1,13 @@ /* Shape inference for untyped npm JavaScript - * (results/design-shape-inference/DESIGN.md). Stage S1: report only. */ + * (results/design-shape-inference/DESIGN.md): the report (S1) and the typed + * overlay (overlay.ts). */ import type * as ts from "../ts7/adapter.js"; -import { analyzeShapes } from "./analyze.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"; @@ -10,7 +16,8 @@ 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. */ + * 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()); @@ -22,3 +29,134 @@ export function reportShapes(program: ts.Program, filter: string): void { 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/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/ts7/api.ts b/packages/compiler/src/frontend/ts7/api.ts index f18675d0e..4e8ef8291 100644 --- a/packages/compiler/src/frontend/ts7/api.ts +++ b/packages/compiler/src/frontend/ts7/api.ts @@ -12,8 +12,10 @@ export class Ts7Api { constructor(rpc: Ts7RpcClient, fs: Ts7FileSystem, collectTiming = false) { try { - registerTs7FileSystem(rpc, fs); - this.session = new Ts7Session(rpc, collectTiming); + const session = new Ts7Session(rpc, collectTiming); + // Callbacks run inside requests, after initialize chose the protocol. + registerTs7FileSystem(rpc, fs, () => session.speaksTs71()); + this.session = session; } catch (error) { rpc.close(); throw error; 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..14b1c4548 100644 --- a/packages/compiler/src/frontend/ts7/ast-wire.ts +++ b/packages/compiler/src/frontend/ts7/ast-wire.ts @@ -123,6 +123,49 @@ 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; + +/** A TypeScript 7.0 SyntaxKind as TypeScript 7.1 numbers it. */ +export function syntaxKind71(kind: number): number { + return kind >= SOURCE_KEYWORD_7_1 ? kind + 1 : kind; +} + +/** 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 +182,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 +200,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 +226,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 +250,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 ? syntaxKind71(kind) : 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.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index cb4a2e5fb..7e7227437 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -7,6 +7,7 @@ import { Ts7RpcClient } from "./rpc-client.js"; import { registerTs7FileSystem, TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { ts7Executable } from "./rpc-api.js"; +import { ts7TestServers, type Ts7TestServer } from "./test-servers.js"; import { Ts7Session } from "./session.js"; import { SemanticChecker } from "./semantic-checker.js"; import { SemanticSnapshot } from "./semantic-model.js"; @@ -18,26 +19,36 @@ import type { Identifier, SourceFile } from "./ast-types.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); -function connect(files: Record) { +/** The default checker (tsc-ts) and tsc 7.1. */ +const servers: Ts7TestServer[] = [ + { name: "tsc 7.0", executable: ts7Executable(), ts71: false }, + ...ts7TestServers().filter((server) => server.ts71), +]; + +function connect(server: Ts7TestServer, files: Record) { const dir = mkdtempSync(join(tempRoot, "scriptc-checker-lifetime-")); const paths = new Map( Object.entries(files).map(([name, text]) => [protocolPath(join(dir, name)), text]), ); const rpc = new Ts7RpcClient( - spawnTs7Wire(ts7Executable(), [ + spawnTs7Wire(server.executable, [ "--api", "--cwd", dir, `--callbacks=${TS7_FILE_SYSTEM_CALLBACKS}`, ]), ); - registerTs7FileSystem(rpc, { - readFile: (path) => paths.get(path), - fileExists: (path) => (paths.has(path) ? true : undefined), - directoryExists: () => undefined, - realpath: () => undefined, - getAccessibleEntries: () => undefined, - }); + registerTs7FileSystem( + rpc, + { + readFile: (path) => paths.get(path), + fileExists: (path) => (paths.has(path) ? true : undefined), + directoryExists: () => undefined, + realpath: () => undefined, + getAccessibleEntries: () => undefined, + }, + () => server.ts71, + ); const session = new Ts7Session(rpc); return { session, @@ -65,184 +76,188 @@ const config = JSON.stringify({ files: ["main.ts"], }); -test("unchanged AST identities receive independent semantic answers after a dependency update", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": 'import { value } from "./dependency.js"; export const result = value;', - "dependency.ts": "export const value: number = 1;", - }); - try { - const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const oldProject = first.getProjects()[0]!; - const root = oldProject.program.getSourceFile(h.path("main.ts"))!; - const name = declaration(root, "result").name!; - const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project }); - const oldType = old.getTypeAtLocation(name); - const oldSymbol = old.getSymbolAtLocation(name)!; - expect(old.typeToString(oldType)).toBe("number"); - expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); - - h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); - const second = h.session.updateSnapshot({ - fileChanges: { changed: [h.path("dependency.ts")] }, +for (const server of servers) + test(`unchanged AST identities receive independent semantic answers after a dependency update (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": 'import { value } from "./dependency.js"; export const result = value;', + "dependency.ts": "export const value: number = 1;", }); - const newProject = second.getProjects()[0]!; - const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!; - expect(nextRoot).toBe(root); - expect(declaration(nextRoot, "result").name).toBe(name); - const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project }); - const newType = next.getTypeAtLocation(name); - expect(next.typeToString(newType)).toBe("string"); - expect(newType).not.toBe(oldType); - expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol); - expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number"); - first.dispose(); - expect(() => old.getTypeAtLocation(name)).toThrow("disposed"); - expect(next.getTypeAtLocation(name)).toBe(newType); - expect(next.typeToString(newType)).toBe("string"); - second.dispose(); - expect(() => next.getTypeAtLocation(name)).toThrow("disposed"); - } finally { - h.close(); - } -}); + try { + const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const oldProject = first.getProjects()[0]!; + const root = oldProject.program.getSourceFile(h.path("main.ts"))!; + const name = declaration(root, "result").name!; + const old = new CheckerFacade(oldProject.checker, { project: oldProject.checker.project }); + const oldType = old.getTypeAtLocation(name); + const oldSymbol = old.getSymbolAtLocation(name)!; + expect(old.typeToString(oldType)).toBe("number"); + expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); -test("project-local type handles never alias across simultaneous checker caches", () => { - const h = connect({ - "first.json": config, - "second.json": config, - "main.ts": "export type Choice = string | number; export const value = 42;", + h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); + const second = h.session.updateSnapshot({ + fileChanges: { changed: [h.path("dependency.ts")] }, + }); + const newProject = second.getProjects()[0]!; + const nextRoot = newProject.program.getSourceFile(h.path("main.ts"))!; + expect(nextRoot).toBe(root); + expect(declaration(nextRoot, "result").name).toBe(name); + const next = new CheckerFacade(newProject.checker, { project: newProject.checker.project }); + const newType = next.getTypeAtLocation(name); + expect(next.typeToString(newType)).toBe("string"); + expect(newType).not.toBe(oldType); + expect(next.getSymbolAtLocation(name)).not.toBe(oldSymbol); + expect(old.typeToString(old.getTypeAtLocation(name))).toBe("number"); + first.dispose(); + expect(() => old.getTypeAtLocation(name)).toThrow("disposed"); + expect(next.getTypeAtLocation(name)).toBe(newType); + expect(next.typeToString(newType)).toBe("string"); + second.dispose(); + expect(() => next.getTypeAtLocation(name)).toThrow("disposed"); + } finally { + h.close(); + } }); - try { - const snapshot = h.session.updateSnapshot({ - openProjects: [h.path("first.json"), h.path("second.json")], + +for (const server of servers) + test(`project-local type handles never alias across simultaneous checker caches (${server.name})`, () => { + const h = connect(server, { + "first.json": config, + "second.json": config, + "main.ts": "export type Choice = string | number; export const value = 42;", }); - const first = snapshot.getProject(h.path("first.json"))!; - const second = snapshot.getProject(h.path("second.json"))!; - const root = first.program.getSourceFile(h.path("main.ts"))!; - expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root); - const a = new CheckerFacade(first.checker, { project: first.checker.project }); - const b = new CheckerFacade(second.checker, { project: second.checker.project }); - const name = declaration(root, "value").name!; - const firstType = a.getTypeAtLocation(name); - const secondType = b.getTypeAtLocation(name); - expect(firstType.project).not.toBe(secondType.project); - expect(firstType).not.toBe(secondType); - expect(a.typeToString(firstType)).toBe(b.typeToString(secondType)); - const alias = declaration(root, "Choice").type!; - const firstArms = constituentTypes(a.getTypeFromTypeNode(alias)); - const secondArms = constituentTypes(b.getTypeFromTypeNode(alias)); - expect(firstArms).not.toBe(secondArms); - expect(firstArms.map((arm) => a.typeToString(arm))).toEqual( - secondArms.map((arm) => b.typeToString(arm)), - ); - first.checker.project.dispose(); - expect(() => a.getTypeAtLocation(name)).toThrow("disposed"); - expect(b.getTypeAtLocation(name)).toBe(secondType); - expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms); - } finally { - h.close(); - } -}); - -test("all public query and prefetch entry points reject a disposed facade before returning cached or local answers", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": "export const value = 42; export function fn(v: number): number { return v; }", + try { + const snapshot = h.session.updateSnapshot({ + openProjects: [h.path("first.json"), h.path("second.json")], + }); + const first = snapshot.getProject(h.path("first.json"))!; + const second = snapshot.getProject(h.path("second.json"))!; + const root = first.program.getSourceFile(h.path("main.ts"))!; + expect(second.program.getSourceFile(h.path("main.ts"))).toBe(root); + const a = new CheckerFacade(first.checker, { project: first.checker.project }); + const b = new CheckerFacade(second.checker, { project: second.checker.project }); + const name = declaration(root, "value").name!; + const firstType = a.getTypeAtLocation(name); + const secondType = b.getTypeAtLocation(name); + expect(firstType.project).not.toBe(secondType.project); + expect(firstType).not.toBe(secondType); + expect(a.typeToString(firstType)).toBe(b.typeToString(secondType)); + const alias = declaration(root, "Choice").type!; + const firstArms = constituentTypes(a.getTypeFromTypeNode(alias)); + const secondArms = constituentTypes(b.getTypeFromTypeNode(alias)); + expect(firstArms).not.toBe(secondArms); + expect(firstArms.map((arm) => a.typeToString(arm))).toEqual( + secondArms.map((arm) => b.typeToString(arm)), + ); + first.checker.project.dispose(); + expect(() => a.getTypeAtLocation(name)).toThrow("disposed"); + expect(b.getTypeAtLocation(name)).toBe(secondType); + expect(b.getTypeFromTypeNode(alias).getTypes()).toBe(secondArms); + } finally { + h.close(); + } }); - try { - const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = snapshot.getProjects()[0]!; - const root = project.program.getSourceFile(h.path("main.ts"))!; - const facade = new CheckerFacade(project.checker, { project: project.checker.project }); - const name = declaration(root, "value").name!; - const fn = declaration(root, "fn"); - const type = facade.getTypeAtLocation(name); - const symbol = facade.getSymbolAtLocation(name)!; - const signature = facade.getSignatureFromDeclaration(fn)!; - const never = project.checker.getNeverType(); - facade.getUnknownType(); - facade.typeToString(type); - facade.prefetchSourceFile(root); - const calls: Record unknown> = { - getCallSignatures: () => facade.getCallSignatures(type), - getConstructSignatures: () => facade.getConstructSignatures(type), - getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), - resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), - getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), - getAnyType: () => facade.getAnyType(), - getNeverType: () => facade.getNeverType(), - getBigIntType: () => facade.getBigIntType(), - getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), - prefetchSourceFile: () => facade.prefetchSourceFile(root), - prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), - prefetchRoots: () => facade.prefetchRoots([]), - prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]), - prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]), - prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]), - prefetchClassCollection: () => facade.prefetchClassCollection([], []), - getTypeAtLocation: () => facade.getTypeAtLocation(name), - getSymbolAtLocation: () => facade.getSymbolAtLocation(name), - getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol), - prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0), - getAliasedSymbol: () => facade.getAliasedSymbol(symbol), - getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol), - getContextualType: () => facade.getContextualType(name), - getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!), - getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name), - getResolvedSignature: () => facade.getResolvedSignature(name), - getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn), - getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature), - getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature), - getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type), - getConstantValue: () => facade.getConstantValue(name), - getNonNullableType: () => facade.getNonNullableType(type), - getPropertiesOfType: () => facade.getPropertiesOfType(type), - getBaseTypes: () => facade.getBaseTypes(type), - isNeverType: () => facade.isNeverType(never), - isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type), - getIndexInfosOfType: () => facade.getIndexInfosOfType(type), - getTypeArguments: () => facade.getTypeArguments(type), - isArrayType: () => facade.isArrayType(type), - isTupleType: () => facade.isTupleType(type), - isArrayLikeType: () => facade.isArrayLikeType(type), - typeToString: () => facade.typeToString(type), - getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name), - getUnknownType: () => facade.getUnknownType(), - getStringType: () => facade.getStringType(), - getNumberType: () => facade.getNumberType(), - getBooleanType: () => facade.getBooleanType(), - getAwaitedType: () => facade.getAwaitedType(type), - }; - const before = h.rpc.timing().requests; - facade.dispose(); - facade.dispose(); - for (const [name, call] of Object.entries(calls)) - expect(call, name).toThrow("checker facade is disposed"); - expect(h.rpc.timing().requests).toBe(before); - expect(project.checker.typeToString(type)).toBe("42"); - } finally { - h.close(); - } -}); -test("session shutdown invalidates every retained facade without explicit program disposal", () => { - const h = connect({ "tsconfig.json": config, "main.ts": "export const value = 1;" }); - try { - const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = snapshot.getProjects()[0]!; - const facade = new CheckerFacade(project.checker); - const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!; - facade.getTypeAtLocation(name); - facade.getNumberType(); - h.session.close(); - expect(() => facade.getTypeAtLocation(name)).toThrow("disposed"); - expect(() => facade.getNumberType()).toThrow("disposed"); - expect(() => new CheckerFacade(project.checker)).toThrow("disposed"); - } finally { - h.close(); - } -}); +for (const server of servers) + test(`all public query and prefetch entry points reject a disposed facade before returning cached or local answers (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": "export const value = 42; export function fn(v: number): number { return v; }", + }); + try { + const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = snapshot.getProjects()[0]!; + const root = project.program.getSourceFile(h.path("main.ts"))!; + const facade = new CheckerFacade(project.checker, { project: project.checker.project }); + const name = declaration(root, "value").name!; + const fn = declaration(root, "fn"); + const type = facade.getTypeAtLocation(name); + const symbol = facade.getSymbolAtLocation(name)!; + const signature = facade.getSignatureFromDeclaration(fn)!; + const never = project.checker.getNeverType(); + facade.getUnknownType(); + facade.typeToString(type); + facade.prefetchSourceFile(root); + const calls: Record unknown> = { + getCallSignatures: () => facade.getCallSignatures(type), + getConstructSignatures: () => facade.getConstructSignatures(type), + getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), + resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), + getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), + getAnyType: () => facade.getAnyType(), + getNeverType: () => facade.getNeverType(), + getBigIntType: () => facade.getBigIntType(), + getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), + prefetchSourceFile: () => facade.prefetchSourceFile(root), + prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), + prefetchRoots: () => facade.prefetchRoots([]), + prefetchSymbolRoots: () => facade.prefetchSymbolRoots([]), + prefetchSymbolNodesExact: () => facade.prefetchSymbolNodesExact([]), + prefetchCollectionTypes: () => facade.prefetchCollectionTypes([]), + prefetchClassCollection: () => facade.prefetchClassCollection([], []), + getTypeAtLocation: () => facade.getTypeAtLocation(name), + getSymbolAtLocation: () => facade.getSymbolAtLocation(name), + getTypeOfSymbol: () => facade.getTypeOfSymbol(symbol), + prefetchMemberTypes: () => facade.prefetchMemberTypes([symbol], 0), + getAliasedSymbol: () => facade.getAliasedSymbol(symbol), + getDeclaredTypeOfSymbol: () => facade.getDeclaredTypeOfSymbol(symbol), + getContextualType: () => facade.getContextualType(name), + getTypeFromTypeNode: () => facade.getTypeFromTypeNode(fn.type!), + getShorthandAssignmentValueSymbol: () => facade.getShorthandAssignmentValueSymbol(name), + getResolvedSignature: () => facade.getResolvedSignature(name), + getSignatureFromDeclaration: () => facade.getSignatureFromDeclaration(fn), + getReturnTypeOfSignature: () => facade.getReturnTypeOfSignature(signature), + getTypePredicateOfSignature: () => facade.getTypePredicateOfSignature(signature), + getBaseTypeOfLiteralType: () => facade.getBaseTypeOfLiteralType(type), + getConstantValue: () => facade.getConstantValue(name), + getNonNullableType: () => facade.getNonNullableType(type), + getPropertiesOfType: () => facade.getPropertiesOfType(type), + getBaseTypes: () => facade.getBaseTypes(type), + isNeverType: () => facade.isNeverType(never), + isTypeAssignableTo: () => facade.isTypeAssignableTo(type, type), + getIndexInfosOfType: () => facade.getIndexInfosOfType(type), + getTypeArguments: () => facade.getTypeArguments(type), + isArrayType: () => facade.isArrayType(type), + isTupleType: () => facade.isTupleType(type), + isArrayLikeType: () => facade.isArrayLikeType(type), + typeToString: () => facade.typeToString(type), + getTypeOfSymbolAtLocation: () => facade.getTypeOfSymbolAtLocation(symbol, name), + getUnknownType: () => facade.getUnknownType(), + getStringType: () => facade.getStringType(), + getNumberType: () => facade.getNumberType(), + getBooleanType: () => facade.getBooleanType(), + getAwaitedType: () => facade.getAwaitedType(type), + }; + const before = h.rpc.timing().requests; + facade.dispose(); + facade.dispose(); + for (const [name, call] of Object.entries(calls)) + expect(call, name).toThrow("checker facade is disposed"); + expect(h.rpc.timing().requests).toBe(before); + expect(project.checker.typeToString(type)).toBe("42"); + } finally { + h.close(); + } + }); + +for (const server of servers) + test(`session shutdown invalidates every retained facade without explicit program disposal (${server.name})`, () => { + const h = connect(server, { "tsconfig.json": config, "main.ts": "export const value = 1;" }); + try { + const snapshot = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = snapshot.getProjects()[0]!; + const facade = new CheckerFacade(project.checker); + const name = declaration(project.program.getSourceFile(h.path("main.ts"))!, "value").name!; + facade.getTypeAtLocation(name); + facade.getNumberType(); + h.session.close(); + expect(() => facade.getTypeAtLocation(name)).toThrow("disposed"); + expect(() => facade.getNumberType()).toThrow("disposed"); + expect(() => new CheckerFacade(project.checker)).toThrow("disposed"); + } finally { + h.close(); + } + }); test("program disposal and host close seal cached source and checker access", () => { const dir = mkdtempSync(join(tempRoot, "scriptc-program-lifetime-")); @@ -356,37 +371,38 @@ test("property lookup caches hits and misses by type and name within each projec snapshot.dispose(); }); -test("dependency edits invalidate positive and negative property answers", () => { - const h = connect({ - "tsconfig.json": config, - "main.ts": 'import { value } from "./dependency.js"; export const result = value;', - "dependency.ts": "export const value = { before: 1 };", - }); - try { - const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); - const project = first.getProjects()[0]!; - const root = project.program.getSourceFile(h.path("main.ts"))!; - const old = new CheckerFacade(project.checker); - const type = old.getTypeAtLocation(declaration(root, "result").name!); - expect(old.getPropertyOfType(type, "before")).toBeDefined(); - expect(old.getPropertyOfType(type, "after")).toBeUndefined(); - h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };"); - const second = h.session.updateSnapshot({ - fileChanges: { changed: [h.path("dependency.ts")] }, +for (const server of servers) + test(`dependency edits invalidate positive and negative property answers (${server.name})`, () => { + const h = connect(server, { + "tsconfig.json": config, + "main.ts": 'import { value } from "./dependency.js"; export const result = value;', + "dependency.ts": "export const value = { before: 1 };", }); - const nextProject = second.getProjects()[0]!; - const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!; - expect(nextRoot).toBe(root); - const next = new CheckerFacade(nextProject.checker); - const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!); - expect(next.getPropertyOfType(nextType, "before")).toBeUndefined(); - expect(next.getPropertyOfType(nextType, "after")).toBeDefined(); - expect(old.getPropertyOfType(type, "before")).toBeDefined(); - expect(old.getPropertyOfType(type, "after")).toBeUndefined(); - } finally { - h.close(); - } -}); + try { + const first = h.session.updateSnapshot({ openProjects: [h.path("tsconfig.json")] }); + const project = first.getProjects()[0]!; + const root = project.program.getSourceFile(h.path("main.ts"))!; + const old = new CheckerFacade(project.checker); + const type = old.getTypeAtLocation(declaration(root, "result").name!); + expect(old.getPropertyOfType(type, "before")).toBeDefined(); + expect(old.getPropertyOfType(type, "after")).toBeUndefined(); + h.paths.set(h.path("dependency.ts"), "export const value = { after: 2 };"); + const second = h.session.updateSnapshot({ + fileChanges: { changed: [h.path("dependency.ts")] }, + }); + const nextProject = second.getProjects()[0]!; + const nextRoot = nextProject.program.getSourceFile(h.path("main.ts"))!; + expect(nextRoot).toBe(root); + const next = new CheckerFacade(nextProject.checker); + const nextType = next.getTypeAtLocation(declaration(nextRoot, "result").name!); + expect(next.getPropertyOfType(nextType, "before")).toBeUndefined(); + expect(next.getPropertyOfType(nextType, "after")).toBeDefined(); + expect(old.getPropertyOfType(type, "before")).toBeDefined(); + expect(old.getPropertyOfType(type, "after")).toBeUndefined(); + } finally { + h.close(); + } + }); test("member type batches are bounded, reuse warm entries, and isolate absent or panicking fields", () => { const calls: number[][] = []; diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 11bb8ef3a..2c8ff873f 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -47,7 +47,7 @@ import { } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; import { typePatchedText, typePatchFileExists } from "../type-patches/layer.js"; -import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.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"; @@ -439,10 +439,11 @@ export class Ts7Program { ): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { this.snapshot.ensureActive(); const program = this.project.program; - const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + 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 diff --git a/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts b/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts index 858a5a9e5..b63a66cb1 100644 --- a/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts +++ b/packages/compiler/src/frontend/ts7/rpc-filesystem.test.ts @@ -15,7 +15,12 @@ const fallback: Ts7FileSystem = { getAccessibleEntries: () => undefined, }; -function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/file.ts")) { +function callback( + name: string, + fs: Ts7FileSystem, + payload = JSON.stringify("/file.ts"), + ts71 = false, +) { const incoming: number[] = []; const encoded = new Ts7Wire({ read: () => 0, @@ -47,7 +52,7 @@ function callback(name: string, fs: Ts7FileSystem, payload = JSON.stringify("/fi }, }), ); - registerTs7FileSystem(client, fs); + registerTs7FileSystem(client, fs, () => ts71); let error: unknown; try { client.requestText("query", "null"); @@ -88,6 +93,36 @@ test.each(TS7_FILE_SYSTEM_CALLBACKS.split(","))("%s delegates undefined to the s expect(callback(name, fallback)).toEqual({ kind: 2, text: "", error: undefined, closed: false }); }); +test("TypeScript 7.1 replies carry the same answers as value, missing and useOS", () => { + const reply = (name: string, fs: Ts7FileSystem) => + JSON.parse(callback(name, fs, JSON.stringify("/file.ts"), true).text) as unknown; + expect(reply("readFile", { ...fallback, readFile: () => "text" })).toEqual({ + kind: "value", + value: "text", + }); + expect(reply("readFile", { ...fallback, readFile: () => "" })).toEqual({ + kind: "value", + value: "", + }); + expect(reply("readFile", { ...fallback, readFile: () => null })).toEqual({ kind: "missing" }); + for (const name of TS7_FILE_SYSTEM_CALLBACKS.split(",")) + expect(reply(name, fallback), name).toEqual({ kind: "useOS" }); + expect(reply("fileExists", { ...fallback, fileExists: () => false })).toEqual({ + kind: "value", + value: false, + }); + expect(reply("realpath", { ...fallback, realpath: () => "/real.ts" })).toEqual({ + kind: "value", + value: "/real.ts", + }); + expect( + reply("getAccessibleEntries", { + ...fallback, + getAccessibleEntries: () => ({ files: ["a.ts"], directories: [] }), + }), + ).toEqual({ kind: "value", value: { files: ["a.ts"], directories: [] } }); +}); + test("boolean false and empty directory entries do not fall through", () => { const fs = { ...fallback, diff --git a/packages/compiler/src/frontend/ts7/rpc-filesystem.ts b/packages/compiler/src/frontend/ts7/rpc-filesystem.ts index 601e4473c..f1fcf2265 100644 --- a/packages/compiler/src/frontend/ts7/rpc-filesystem.ts +++ b/packages/compiler/src/frontend/ts7/rpc-filesystem.ts @@ -18,31 +18,50 @@ function pathArgument(payload: string): string { return value; } +/** A TypeScript 7.1 callback reply: the value, `missing` for a path the + * client hides, or `useOS` to delegate to the server's file system. */ +function reply71(result: unknown): string { + if (result === undefined) return '{"kind":"useOS"}'; + if (result === null) return '{"kind":"missing"}'; + return JSON.stringify({ kind: "value", value: result }); +} + /** Preserve all three readFile answers. An empty reply delegates to the * native server's filesystem; {content:null} hides a path; {content:""} - * supplies a present empty file. Conflating these changes module resolution. */ -export function registerTs7FileSystem(client: Ts7RpcClient, fs: Ts7FileSystem): void { + * supplies a present empty file. Conflating these changes module resolution. + * `ts71` tells whether the server speaks TypeScript 7.1's protocol, whose + * replies carry the same three answers as reply71 encodes them. */ +export function registerTs7FileSystem( + client: Ts7RpcClient, + fs: Ts7FileSystem, + ts71: () => boolean = () => false, +): void { client.registerCallback("readFile", (payload) => { const content = fs.readFile(pathArgument(payload)); + if (ts71()) return reply71(content); return content === undefined ? "" : JSON.stringify({ content }); }); client.registerCallback("fileExists", (payload) => { const result = fs.fileExists(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return result ? "true" : "false"; }); client.registerCallback("directoryExists", (payload) => { const result = fs.directoryExists(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return result ? "true" : "false"; }); client.registerCallback("realpath", (payload) => { const result = fs.realpath(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return JSON.stringify(result); }); client.registerCallback("getAccessibleEntries", (payload) => { const result = fs.getAccessibleEntries(pathArgument(payload)); + if (ts71()) return reply71(result); if (result === undefined) return ""; return JSON.stringify(result!); }); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index 90a7a25cf..766357b56 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -6,9 +6,9 @@ import { API } from "typescript/unstable/sync"; import type { Node as SdkNode } from "typescript/unstable/ast"; import { AstNode } from "./ast-node.js"; import { Ts7RpcClient } from "./rpc-client.js"; -import { tsgoExecutable } from "./rpc-api.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { Ts7Session } from "./session.js"; +import { ts7TestServers } from "./test-servers.js"; import { SyntaxKind } from "./enums.js"; import { getSourceFileOfNode } from "./node-functions.js"; import { forEachChild } from "./ast.js"; @@ -25,12 +25,14 @@ const source = [ "export function predicate(value: unknown): value is Box { return value instanceof Box; }", "export function rest(...values: number[]): number { return values.length; }", "export function receiver(this: Box, value: number): number { return value; }", + 'export enum Kind { A = 1, B = "b", C = A + 2, D }', ].join("\n"); let directory: string; let file: string; let config: string; -let session: Ts7Session; +const servers = ts7TestServers(); +const sessions = new Map(); let oracle: API; beforeAll(() => { @@ -47,15 +49,19 @@ beforeAll(() => { files: [file], }), ); - // Compared with the pinned SDK, which runs tsgo. - session = new Ts7Session( - new Ts7RpcClient(spawnTs7Wire(tsgoExecutable(), ["--api", "--cwd", directory])), - ); + // Compared with the pinned SDK, which runs tsgo 7.0; tsc 7.1 answers these cases the same. + for (const server of servers) + sessions.set( + server.name, + new Ts7Session( + new Ts7RpcClient(spawnTs7Wire(server.executable, ["--api", "--cwd", directory])), + ), + ); oracle = new API({ cwd: directory }); }); afterAll(() => { - session?.close(); + for (const session of sessions.values()) session.close(); oracle?.close(); if (directory) rmSync(directory, { recursive: true, force: true }); }); @@ -75,82 +81,115 @@ function shape(node: AstNode | SdkNode): unknown { }; } -test("synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields", () => { - const snapshot = session.updateSnapshot({ openProjects: [config] }); - const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); - try { - const project = snapshot.getProject(config)!; - const sdk = sdkSnapshot.getProject(config)!; - const root = project.program.getSourceFile(file)!; - const expectedRoot = sdk.program.getSourceFile(file)!; - let checked = 0; - for (let index = 1; index < 7; index++) { - const statement = root.statements.nodes[index]!; - const expectedStatement = expectedRoot.statements[index]!; - const node = statement.declarationList!.declarations!.nodes[0]!.name!; - const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] - .name as SdkNode; - const type = project.checker.getTypeAtLocation(node)!; - const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!; - const actual = project.checker.typeToTypeNode(type, node)!; - const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!; - expect(shape(actual)).toEqual(shape(expected)); - expect(actual.kind).not.toBe(SyntaxKind.SourceFile); - expect(actual.file.root).toBe(actual); - expect(actual.file.node(1)).toBe(actual); - expect(actual.parent).toBeUndefined(); - expect(() => actual.file.sourceFile).toThrow("expected a source file root"); - expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); - forEachChild(actual, (child) => { - expect(child.parent).toBe(actual); - }); - checked++; - } - expect(checked).toBe(6); - expect(root.file.sourceFile).toBe(root); - expect(getSourceFileOfNode(root)).toBe(root); - } finally { - snapshot.dispose(); - sdkSnapshot.dispose(); - } -}); - -test("generic, predicate, rest and this-parameter signature fragments match the pinned SDK", () => { - const snapshot = session.updateSnapshot({ openProjects: [config] }); - const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); - try { - const project = snapshot.getProject(config)!; - const sdk = sdkSnapshot.getProject(config)!; - const root = project.program.getSourceFile(file)!; - const expectedRoot = sdk.program.getSourceFile(file)!; - for (let index = 7; index < 11; index++) { - const node = root.statements.nodes[index]!; - const expectedNode = expectedRoot.statements[index]!; - const signature = project.checker.getSignatureFromDeclaration(node)!; - const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; - const actual = project.checker.signatureToSignatureDeclaration( - signature, - SyntaxKind.FunctionType, - node, - )!; - const expected = sdk.checker.signatureToSignatureDeclaration( - expectedSignature, - SyntaxKind.FunctionType, - expectedNode, - )!; - expect(actual.kind).toBe(SyntaxKind.FunctionType); - expect(shape(actual)).toEqual(shape(expected)); - expect(actual.file.root).toBe(actual); - const parameters = actual.parameters!.nodes; - expect(parameters.length).toBeGreaterThan(0); - for (const parameter of parameters) { - expect(parameter.parent).toBe(actual); - expect(actual.file.node(parameter.index)).toBe(parameter); +for (const server of servers) + test(`synthetic type roots and descendants match the pinned SDK without claiming SourceFile fields (${server.name})`, () => { + const session = sessions.get(server.name)!; + const snapshot = session.updateSnapshot({ openProjects: [config] }); + const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); + try { + const project = snapshot.getProject(config)!; + const sdk = sdkSnapshot.getProject(config)!; + const root = project.program.getSourceFile(file)!; + const expectedRoot = sdk.program.getSourceFile(file)!; + let checked = 0; + for (let index = 1; index < 7; index++) { + const statement = root.statements.nodes[index]!; + const expectedStatement = expectedRoot.statements[index]!; + const node = statement.declarationList!.declarations!.nodes[0]!.name!; + const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] + .name as SdkNode; + const type = project.checker.getTypeAtLocation(node)!; + const expectedType = sdk.checker.getTypeAtLocation(expectedNode)!; + const actual = project.checker.typeToTypeNode(type, node)!; + const expected = sdk.checker.typeToTypeNode(expectedType, expectedNode)!; + expect(shape(actual)).toEqual(shape(expected)); + expect(actual.kind).not.toBe(SyntaxKind.SourceFile); + expect(actual.file.root).toBe(actual); + expect(actual.file.node(1)).toBe(actual); + expect(actual.parent).toBeUndefined(); + expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); + forEachChild(actual, (child) => { + expect(child.parent).toBe(actual); + }); + checked++; } - expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + expect(checked).toBe(6); + expect(root.file.sourceFile).toBe(root); + expect(getSourceFileOfNode(root)).toBe(root); + } finally { + snapshot.dispose(); + sdkSnapshot.dispose(); } - } finally { - snapshot.dispose(); - sdkSnapshot.dispose(); - } -}); + }); + +for (const server of servers) + test(`generic, predicate, rest and this-parameter signature fragments match the pinned SDK (${server.name})`, () => { + const session = sessions.get(server.name)!; + const snapshot = session.updateSnapshot({ openProjects: [config] }); + const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); + try { + const project = snapshot.getProject(config)!; + const sdk = sdkSnapshot.getProject(config)!; + const root = project.program.getSourceFile(file)!; + const expectedRoot = sdk.program.getSourceFile(file)!; + for (let index = 7; index < 11; index++) { + const node = root.statements.nodes[index]!; + const expectedNode = expectedRoot.statements[index]!; + const signature = project.checker.getSignatureFromDeclaration(node)!; + const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; + const actual = project.checker.signatureToSignatureDeclaration( + signature, + SyntaxKind.FunctionType, + node, + )!; + const expected = sdk.checker.signatureToSignatureDeclaration( + expectedSignature, + SyntaxKind.FunctionType, + expectedNode, + )!; + expect(actual.kind).toBe(SyntaxKind.FunctionType); + expect(shape(actual)).toEqual(shape(expected)); + expect(actual.file.root).toBe(actual); + const parameters = actual.parameters!.nodes; + expect(parameters.length).toBeGreaterThan(0); + for (const parameter of parameters) { + expect(parameter.parent).toBe(actual); + expect(actual.file.node(parameter.index)).toBe(parameter); + } + expect(() => actual.file.sourceFile).toThrow("expected a source file root"); + } + } finally { + snapshot.dispose(); + sdkSnapshot.dispose(); + } + }); + +for (const server of servers) + test(`enum member constant values match the pinned SDK (${server.name})`, () => { + const session = sessions.get(server.name)!; + const snapshot = session.updateSnapshot({ openProjects: [config] }); + const sdkSnapshot = oracle.updateSnapshot({ openProjects: [config] }); + try { + const project = snapshot.getProject(config)!; + const sdk = sdkSnapshot.getProject(config)!; + const statement = project.program.getSourceFile(file)!.statements.nodes[11]!; + const expected = Reflect.get( + sdk.program.getSourceFile(file)!.statements[11]!, + "members", + ) as SdkNode[]; + const members = statement.members!.nodes; + expect(members.map((member) => project.checker.getConstantValue(member))).toEqual( + expected.map((member) => sdk.checker.getConstantValue(member)), + ); + expect(members.map((member) => project.checker.getConstantValue(member))).toEqual([ + 1, + "b", + 3, + 4, + ]); + } finally { + snapshot.dispose(); + sdkSnapshot.dispose(); + } + }); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index c19f93598..3dc0dd7dd 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -12,6 +12,8 @@ import { SemanticType, type SemanticDocument, type SemanticQuery, + type Ts71SymbolData, + type Ts71TypeData, present, } from "./semantic-model.js"; import { SemanticSymbolFlags } from "./semantic-schema.generated.js"; @@ -72,7 +74,8 @@ export interface SemanticCompletionOptions { triggerCharacter?: string; includeSymbol?: boolean; } -interface CompletionResponse { +/** `S` is TypeScript 7.0's SymbolResponse or 7.1's Ts71SymbolData. */ +interface CompletionResponse { isIncomplete: boolean; entries: { name: string; @@ -82,9 +85,36 @@ interface CompletionResponse { filterText?: string; detail?: string; labelDetails?: { detail?: string; description?: string }; - symbol?: SymbolResponse; + symbol?: S; }[]; } + +/** TypeScript 7.1's type predicate and index info replies. */ +interface TypePredicateResponse71 { + kind: number; + parameterIndex: number; + parameterName?: string; + type?: Ts71TypeData; +} +interface IndexInfoResponse71 { + keyType: Ts71TypeData; + valueType: Ts71TypeData; + isReadonly?: boolean; + declaration?: string; +} + +/** TypeScript 7.1's getConstantValue answer: the value, with a number's + * non-finite values written as "+Infinity", "-Infinity" and "NaN". */ +interface ConstantValueResponse71 { + isNumber: boolean; + value?: string | number | boolean | null; +} + +interface ReferencedSymbolResponse { + definition: string; + symbol?: S; + references?: string[]; +} export interface SemanticCompletion { name: string; kind: number | undefined; @@ -369,6 +399,7 @@ export class SemanticChecker { return this.project.request("isArrayLikeType", { type: type.id }); } isTupleType(type: SemanticType): boolean { + if (this.project.ts71()) return this.project.isTupleType71(type.id); return this.project.request("isTupleType", { type: type.id }); } isTypeAssignableTo(source: SemanticType, target: SemanticType): boolean { @@ -378,6 +409,16 @@ export class SemanticChecker { }); } getConstantValue(node: AstNode): string | number | undefined { + if (this.project.ts71()) { + const data = this.project.request("getConstantValue", { + location: node.id, + }); + const value = data?.value; + if (typeof value === "number") return value; + if (typeof value !== "string") return undefined; + if (!data!.isNumber) return value; + return value === "+Infinity" ? Infinity : value === "-Infinity" ? -Infinity : NaN; + } return ( this.project.request("getConstantValue", { location: node.id }) ?? undefined @@ -390,6 +431,20 @@ export class SemanticChecker { ); } getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined { + if (this.project.ts71()) { + const data = this.project.request( + "getTypePredicateOfSignature", + { signature: signature.id }, + ); + return data === null + ? undefined + : { + kind: data.kind, + parameterIndex: data.parameterIndex, + parameterName: data.parameterName, + type: data.type === undefined ? undefined : this.project.type71(data.type), + }; + } const data = this.project.request("getTypePredicateOfSignature", { signature: signature.id, }); @@ -403,6 +458,22 @@ export class SemanticChecker { }; } getIndexInfosOfType(type: SemanticType): SemanticIndexInfo[] { + if (this.project.ts71()) { + const data = this.project.request("getIndexInfosOfType", { + type: type.id, + }); + return (data ?? []).map( + (item) => + new SemanticIndexInfo( + this.project.type71(item.keyType), + this.project.type71(item.valueType), + item.isReadonly ?? false, + item.declaration === undefined + ? undefined + : new SemanticNodeHandle(item.declaration, this.project), + ), + ); + } const data = this.project.request("getIndexInfosOfType", { type: type.id, }); @@ -420,8 +491,14 @@ export class SemanticChecker { } private getWellKnownSymbols(): WellKnownSymbols { this.project.ensureActive(); - if (this.wellKnown === undefined) - this.wellKnown = this.project.request("getWellKnownSymbols"); + if (this.wellKnown === undefined) { + const ids = this.project.request("getWellKnownSymbols"); + this.wellKnown = { + unknown: this.project.snapshotSymbolId(ids.unknown), + undefined: this.project.snapshotSymbolId(ids.undefined), + arguments: this.project.snapshotSymbolId(ids.arguments), + }; + } return this.wellKnown; } isUnknownSymbol(symbol: SemanticSymbol): boolean { @@ -445,9 +522,24 @@ export class SemanticChecker { return (data ?? []).map((handle) => new SemanticNodeHandle(handle, this.project)); } getReferencedSymbolsForNode(node: AstNode, position: number): SemanticReferencedSymbol[] { - const data = this.project.request< - { definition: string; symbol?: SymbolResponse; references?: string[] }[] | null - >("getReferencedSymbolsForNode", { node: node.id, position }); + const query = { node: node.id, position }; + if (this.project.ts71()) { + const data = this.project.request[] | null>( + "getReferencedSymbolsForNode", + query, + ); + return (data ?? []).map((item) => ({ + definition: new SemanticNodeHandle(item.definition, this.project), + symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol), + references: (item.references ?? []).map( + (handle) => new SemanticNodeHandle(handle, this.project), + ), + })); + } + const data = this.project.request[] | null>( + "getReferencedSymbolsForNode", + query, + ); return (data ?? []).map((item) => ({ definition: new SemanticNodeHandle(item.definition, this.project), symbol: item.symbol === undefined ? undefined : this.project.symbol(item.symbol), @@ -479,12 +571,36 @@ export class SemanticChecker { position: number, options?: SemanticCompletionOptions, ): { isIncomplete: boolean; entries: SemanticCompletion[] } | undefined { - const data = this.project.request("getCompletionsAtPosition", { + const query = { file, position, triggerCharacter: options?.triggerCharacter, includeSymbol: options?.includeSymbol, - }); + }; + if (this.project.ts71()) { + const data = this.project.request | null>( + "getCompletionsAtPosition", + query, + ); + if (data === null) return undefined; + return { + isIncomplete: data.isIncomplete, + entries: data.entries.map((item) => ({ + name: item.name, + kind: item.kind, + sortText: item.sortText, + insertText: item.insertText, + filterText: item.filterText, + detail: item.detail, + labelDetails: item.labelDetails, + symbol: item.symbol === undefined ? undefined : this.project.symbol71(item.symbol), + })), + }; + } + const data = this.project.request | null>( + "getCompletionsAtPosition", + query, + ); if (data === null) return undefined; return { isIncomplete: data.isIncomplete, diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 8913007d3..ea924a039 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -149,7 +149,7 @@ function parseJsonSlice(bytes: Uint8Array, start: number, end: number): T { /** One element of a reply: null, or the object with leading id `id`. Objects * the caller already holds under that id are not decoded. */ -function replyElement( +function replyElement( bytes: Uint8Array, start: number, end: number, @@ -172,7 +172,7 @@ function replyElement( * reply is null. Objects the caller already holds under their id, which a * reply repeats whenever it names them, are returned from `cached` without * decoding their text; the rest are decoded and handed to `build`. */ -export function decodeSemanticObject( +export function decodeSemanticObject( bytes: Uint8Array, cached: (id: number) => R | undefined, build: (data: T) => R, @@ -187,7 +187,7 @@ export function decodeSemanticObject( /** A checker reply that is an array of objects (each with a numeric `id`) and * nulls, or null. An empty reply is null. Elements are resolved as in * decodeSemanticObject. */ -export function decodeSemanticObjects( +export function decodeSemanticObjects( bytes: Uint8Array, cached: (id: number) => R | undefined, build: (data: T) => R, diff --git a/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts b/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts new file mode 100644 index 000000000..62de58443 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/semantic-model-71.test.ts @@ -0,0 +1,137 @@ +import { expect, test } from "vitest"; +import { SemanticChecker } from "./semantic-checker.js"; +import { SemanticSnapshot } from "./semantic-model.js"; +import { SyntaxKind, TypeFlags } from "./enums.js"; + +/** A snapshot on a fake TypeScript 7.1 server: `answer` gets each request's + * method and parsed payload and returns the reply's JSON. */ +function server(answer: (method: string, query: Record) => unknown) { + const requests: { method: string; query: Record }[] = []; + const snapshot = new SemanticSnapshot( + 1, + { + text: (method, payload) => { + const query = JSON.parse(payload) as Record; + requests.push({ method, query }); + return JSON.stringify(answer(method, query) ?? null); + }, + binary(method, payload) { + const text = this.text(method, payload); + // AST replies (typeToTypeNode, signatureToSignatureDeclaration) are empty for none. + return text === "null" ? new Uint8Array() : Buffer.from(text, "utf8"); + }, + }, + true, + ); + const project = snapshot.addProject("project", () => undefined); + return { snapshot, project, checker: new SemanticChecker(project), requests }; +} + +const file = { + fileName: "/main.ts", + path: "/main.ts", + contentHash: "0", + parseOptionsKey: "0", + scriptKind: 3, + nodeId: "77", +}; + +test("TypeScript 7.1 symbol references round-trip through local ids", () => { + const box = { kind: 0, file, id: 1 }; + const parent = { kind: 0, file, id: 2 }; + const { project, checker, requests } = server((method, query) => { + if (method === "getTypeAtPosition") + return { id: 10, flags: TypeFlags.Object, objectFlags: 1, symbol: { id: 1, file: "77" } }; + if (method === "getSymbolOfType") + return { + reference: box, + name: "Box", + flags: 32, + checkFlags: 0, + parent: { id: 2, file: "77" }, + }; + if (method === "getParentOfSymbol") { + expect(query["symbol"]).toEqual(box); + expect(query["objectId"]).toBeUndefined(); + return { reference: parent, name: '"/main"', flags: 512, checkFlags: 0 }; + } + if (method === "getDeclaredTypeOfSymbol") { + expect(query["symbol"]).toEqual(box); + return { id: 11, flags: TypeFlags.Object, isTupleType: true }; + } + if (method === "getExportsOfModule") { + expect(query["symbol"]).toEqual(parent); + return [{ reference: box, name: "Box", flags: 32, checkFlags: 0 }]; + } + throw new Error(`unexpected ${method}`); + }); + const type = checker.getTypeAtPosition("/main.ts", 0)!; + const symbol = type.getSymbol()!; + expect(symbol.escapedName).toBe("Box"); + // The compact reference in the type and the full one in the symbol name one symbol. + expect(project.snapshot.cachedSymbol(symbol.id)).toBe(symbol); + const owner = symbol.getParent()!; + expect(owner.escapedName).toBe('"/main"'); + expect(checker.getExportsOfModule(owner)).toEqual([symbol]); + const declared = checker.getDeclaredTypeOfSymbol(symbol); + expect(checker.isTupleType(declared)).toBe(true); + expect(checker.isTupleType(type)).toBe(false); + expect(requests.map((request) => request.method)).not.toContain("isTupleType"); +}); + +test("TypeScript 7.1 requests name types and signatures as 7.1 does", () => { + const { project, checker, requests } = server((method) => { + if (method === "getApparentType" || method === "getReturnTypeOfSignature") + return { id: 12, flags: TypeFlags.Number }; + if (method === "signatureToSignatureDeclaration") return null; + if (method === "getWellKnownSymbols") return { unknown: 5, undefined: 6, arguments: 7 }; + if (method === "getSymbolAtPosition") + return { + reference: { kind: 1, snapshot: 1, project: "project", id: 6 }, + name: "undefined", + flags: 3, + checkFlags: 0, + }; + throw new Error(`unexpected ${method}`); + }); + const type = project.type({ id: 1, flags: TypeFlags.Object }); + const signature = project.signature({ id: 3, flags: 0 }); + checker.getApparentType(type); + checker.getReturnTypeOfSignature(signature); + checker.signatureToSignatureDeclaration(signature, SyntaxKind.FunctionType); + const queries = new Map(requests.map((request) => [request.method, request.query])); + expect(queries.get("getApparentType")).toMatchObject({ objectId: 1 }); + expect(queries.get("getApparentType")!["type"]).toBeUndefined(); + expect(queries.get("getReturnTypeOfSignature")).toMatchObject({ objectId: 3 }); + // 7.1 numbers every kind from SourceKeyword up one higher. + expect(queries.get("signatureToSignatureDeclaration")).toMatchObject({ + kind: SyntaxKind.FunctionType + 1, + }); + // A well-known symbol id names a symbol of the snapshot. + const undefinedSymbol = checker.getSymbolAtPosition("/main.ts", 0)!; + expect(checker.isUndefinedSymbol(undefinedSymbol)).toBe(true); + expect(checker.isUnknownSymbol(undefinedSymbol)).toBe(false); +}); + +test("TypeScript 7.1 constant values are read from their answer object", () => { + const answers = [ + { isNumber: true, value: 1 }, + { isNumber: false, value: "a" }, + { isNumber: true, value: "+Infinity" }, + { isNumber: true, value: "-Infinity" }, + { isNumber: true, value: "NaN" }, + null, + ]; + let next = 0; + const { checker } = server((method) => { + expect(method).toBe("getConstantValue"); + return answers[next++]; + }); + const node = { id: "2.306./main.ts" } as Parameters[0]; + expect(checker.getConstantValue(node)).toBe(1); + expect(checker.getConstantValue(node)).toBe("a"); + expect(checker.getConstantValue(node)).toBe(Infinity); + expect(checker.getConstantValue(node)).toBe(-Infinity); + expect(checker.getConstantValue(node)).toBeNaN(); + expect(checker.getConstantValue(node)).toBeUndefined(); +}); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 04fa8e637..fb438a661 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -6,7 +6,7 @@ import type { import { AstNode } from "./ast-node.js"; import type { __String } from "./ast-types.js"; import type * as enums from "./enums.generated.js"; -import { parseAstNodeHandle } from "./ast-wire.js"; +import { parseAstNodeHandle, syntaxKind71 } from "./ast-wire.js"; import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js"; import { decodeSemanticObject, decodeSemanticObjects, parseSemanticJson } from "./semantic-json.js"; import { @@ -44,8 +44,9 @@ export interface SemanticQuery { position?: number | undefined; positions?: readonly number[] | undefined; type?: number | undefined; - symbol?: number | undefined; - symbols?: number[] | undefined; + /** A symbol's id; on a TypeScript 7.1 server, its reference (SemanticProject.encode). */ + symbol?: number | Ts71SymbolReference | undefined; + symbols?: (number | Ts71SymbolReference)[] | undefined; signature?: number | undefined; source?: number | undefined; target?: number | undefined; @@ -61,6 +62,214 @@ export interface SemanticQuery { includeSymbol?: boolean | undefined; } +/** A symbol in TypeScript 7.1's protocol: its owner, a source file (kind 0) + * or the snapshot and project (kind 1), and its id within that owner. */ +export interface Ts71SymbolReference { + kind: number; + file?: Ts71SourceFileDescriptor; + snapshot?: number; + project?: string; + id: number; +} + +export interface Ts71SourceFileDescriptor { + fileName: string; + path: string; + contentHash: string; + parseOptionsKey: string; + scriptKind: number; + nodeId: string; +} + +/** A symbol another TypeScript 7.1 response names: its id and, for a symbol + * a source file owns, the file's node ID. Without a file, the response's + * snapshot and project own it. */ +export interface Ts71CompactReference { + id: number; + file?: string; +} + +/** A symbol as TypeScript 7.1 reports it. */ +export interface Ts71SymbolData { + reference: Ts71SymbolReference; + name: string; + flags: number; + checkFlags: number; + declarations?: string[]; + valueDeclaration?: string; + parent?: Ts71CompactReference; + exportSymbol?: Ts71CompactReference; +} + +/** A type as TypeScript 7.1 reports it: TypeResponse with compact symbol + * references. */ +export interface Ts71TypeData { + id: number; + flags: number; + objectFlags?: number; + value?: string | number | boolean | null; + freshType?: number; + regularType?: number; + target?: number; + typeParameters?: number[]; + outerTypeParameters?: number[]; + localTypeParameters?: number[]; + elementFlags?: number[]; + fixedLength?: number; + readonly?: boolean; + objectType?: number; + indexType?: number; + checkType?: number; + extendsType?: number; + baseType?: number; + substConstraint?: number; + texts?: string[]; + intrinsicName?: string; + isThisType?: boolean; + /** Whether the type is a tuple type, which 7.0 answers with isTupleType. */ + isTupleType?: boolean; + aliasTypeArguments?: number[]; + aliasSymbol?: Ts71CompactReference; + symbol?: Ts71CompactReference; +} + +/** A signature as TypeScript 7.1 reports it. */ +export interface Ts71SignatureData { + id: number; + flags: number; + declaration?: string; + typeParameters?: number[]; + parameters?: Ts71CompactReference[]; + thisParameter?: Ts71CompactReference; + target?: number; +} + +/** The methods whose source is a symbol: TypeScript 7.0 names it by + * `objectId`, 7.1 by its reference in `symbol`. */ +const SYMBOL_PROPERTY_METHODS = new Set([ + "getParentOfSymbol", + "getExportSymbolOfSymbol", + "getMembersOfSymbol", + "getExportsOfSymbol", +]); + +/** The methods TypeScript 7.1 gives their type or signature in `objectId`, + * where 7.0 takes it in `type` or `signature`. */ +const OBJECT_ID_METHODS_7_1 = new Map([ + ["getApparentType", "type"], + ["getConstraintOfTypeParameter", "type"], + ["getNonNullableType", "type"], + ["getReturnTypeOfSignature", "signature"], +]); + +/** TypeScript 7.1's symbol references by local id. The semantic model keeps + * TypeScript 7.0's numeric symbol ids; on a 7.1 server, each symbol owner + * and id gets a local id, and requests send the reference back. A compact + * reference gets the local id of the symbol it names, so a later response + * with that symbol's full reference finds the same id. */ +export class Ts71SymbolTable { + private readonly locals = new Map(); + private readonly references: (Ts71SymbolReference | undefined)[] = [undefined]; + + private allocate(key: string): number { + let local = this.locals.get(key); + if (local === undefined) { + local = this.references.length; + this.references.push(undefined); + this.locals.set(key, local); + } + return local; + } + + /** The local id of `reference`, which requests then name it by. */ + fromReference(reference: Ts71SymbolReference): number { + const key = + reference.kind === 0 + ? `f${reference.file?.nodeId ?? ""}:${reference.id}` + : `s${reference.project ?? ""}:${reference.id}`; + const local = this.allocate(key); + this.references[local] = reference; + return local; + } + + /** The local id of a compact reference in a response for `project`. */ + fromCompact(reference: Ts71CompactReference, project: string): number { + return this.allocate( + reference.file !== undefined + ? `f${reference.file}:${reference.id}` + : `s${project}:${reference.id}`, + ); + } + + /** The reference of local id `local`. Throws for a symbol no response has + * described yet. */ + reference(local: number): Ts71SymbolReference { + const reference = this.references[local]; + if (reference === undefined) throw new Error(`Unknown TypeScript 7.1 symbol ${local}`); + return reference; + } + + /** `data` in TypeScript 7.0's shape, with local ids. */ + symbolData(data: Ts71SymbolData, project: string): SymbolResponse { + const result: SymbolResponse = { + id: this.fromReference(data.reference), + project, + name: data.name, + flags: data.flags, + checkFlags: data.checkFlags, + }; + if (data.declarations !== undefined) result.declarations = data.declarations; + if (data.valueDeclaration !== undefined) result.valueDeclaration = data.valueDeclaration; + if (data.parent !== undefined) result.parent = this.fromCompact(data.parent, project); + if (data.exportSymbol !== undefined) + result.exportSymbol = this.fromCompact(data.exportSymbol, project); + return result; + } + + typeData(data: Ts71TypeData, project: string): TypeResponse { + const result: TypeResponse = { id: data.id, flags: data.flags }; + if (data.objectFlags !== undefined) result.objectFlags = data.objectFlags; + if (data.value !== undefined) result.value = data.value; + if (data.freshType !== undefined) result.freshType = data.freshType; + if (data.regularType !== undefined) result.regularType = data.regularType; + if (data.target !== undefined) result.target = data.target; + if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters; + if (data.outerTypeParameters !== undefined) + result.outerTypeParameters = data.outerTypeParameters; + if (data.localTypeParameters !== undefined) + result.localTypeParameters = data.localTypeParameters; + if (data.elementFlags !== undefined) result.elementFlags = data.elementFlags; + if (data.fixedLength !== undefined) result.fixedLength = data.fixedLength; + if (data.readonly !== undefined) result.readonly = data.readonly; + if (data.objectType !== undefined) result.objectType = data.objectType; + if (data.indexType !== undefined) result.indexType = data.indexType; + if (data.checkType !== undefined) result.checkType = data.checkType; + if (data.extendsType !== undefined) result.extendsType = data.extendsType; + if (data.baseType !== undefined) result.baseType = data.baseType; + if (data.substConstraint !== undefined) result.substConstraint = data.substConstraint; + if (data.texts !== undefined) result.texts = data.texts; + if (data.intrinsicName !== undefined) result.intrinsicName = data.intrinsicName; + if (data.isThisType !== undefined) result.isThisType = data.isThisType; + if (data.aliasTypeArguments !== undefined) result.aliasTypeArguments = data.aliasTypeArguments; + if (data.aliasSymbol !== undefined) + result.aliasSymbol = this.fromCompact(data.aliasSymbol, project); + if (data.symbol !== undefined) result.symbol = this.fromCompact(data.symbol, project); + return result; + } + + signatureData(data: Ts71SignatureData, project: string): SignatureResponse { + const result: SignatureResponse = { id: data.id, flags: data.flags }; + if (data.declaration !== undefined) result.declaration = data.declaration; + if (data.typeParameters !== undefined) result.typeParameters = data.typeParameters; + if (data.parameters !== undefined) + result.parameters = data.parameters.map((parameter) => this.fromCompact(parameter, project)); + if (data.thisParameter !== undefined) + result.thisParameter = this.fromCompact(data.thisParameter, project); + if (data.target !== undefined) result.target = data.target; + return result; + } +} + export class SemanticProgram { constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {} getSourceFile(path: string): AstNode | undefined { @@ -77,10 +286,16 @@ export class SemanticSnapshot { private readonly symbols = new Map(); private disposed = false; + /** TypeScript 7.1's symbol references, on a server that speaks 7.1. */ + readonly references: Ts71SymbolTable | undefined; + constructor( readonly id: number, readonly transport: SemanticTransport, - ) {} + ts71 = false, + ) { + this.references = ts71 ? new Ts71SymbolTable() : undefined; + } addProject(id: string, sourceFile: (path: string) => AstNode | undefined): SemanticProject { this.ensureActive(); @@ -128,6 +343,9 @@ export class SemanticSnapshot { export class SemanticProject { private readonly types = new Map(); + /** TypeScript 7.1's isTupleType answers, by type id: 7.1 reports them on + * the type instead of answering an isTupleType request. */ + private readonly tupleTypes = new Map(); private readonly signatures = new Map(); private readonly cleanups: (() => void)[] = []; private disposed = false; @@ -150,24 +368,90 @@ export class SemanticProject { this.cleanups.push(cleanup); } - request(method: string, query: SemanticQuery = {}): T { + /** `query` for `method` in this project's snapshot. On a TypeScript 7.1 + * server, symbol ids become their references, and a symbol property + * method takes its source symbol's reference in `symbol`. */ + private encode(method: string, query: SemanticQuery): string { this.ensureActive(); query.snapshot = this.snapshot.id; query.project = this.id; - return parseSemanticJson(this.snapshot.transport.text(method, JSON.stringify(query))); + const references = this.snapshot.references; + if (references !== undefined) { + if (typeof query.symbol === "number") query.symbol = references.reference(query.symbol); + if (query.symbols !== undefined) + query.symbols = query.symbols.map((symbol) => + typeof symbol === "number" ? references.reference(symbol) : symbol, + ); + if (SYMBOL_PROPERTY_METHODS.has(method) && query.objectId !== undefined) { + query.symbol = references.reference(query.objectId); + query.objectId = undefined; + } + // signatureToSignatureDeclaration's kind is a SyntaxKind. + if (method === "signatureToSignatureDeclaration" && query.kind !== undefined) + query.kind = syntaxKind71(query.kind); + const field = OBJECT_ID_METHODS_7_1.get(method); + if (field === "type") { + query.objectId = query.type; + query.type = undefined; + } else if (field === "signature") { + query.objectId = query.signature; + query.signature = undefined; + } + } + return JSON.stringify(query); + } + + request(method: string, query: SemanticQuery = {}): T { + return parseSemanticJson(this.snapshot.transport.text(method, this.encode(method, query))); } requestBinary(method: string, query: SemanticQuery): Uint8Array { - this.ensureActive(); - query.snapshot = this.snapshot.id; - query.project = this.id; - return this.snapshot.transport.binary(method, JSON.stringify(query)); + return this.snapshot.transport.binary(method, this.encode(method, query)); + } + + /** A type a TypeScript 7.1 reply holds. */ + type71(data: Ts71TypeData): SemanticType { + const references = this.snapshot.references; + if (references === undefined) throw new Error("TypeScript 7.1 type from a 7.0 server"); + this.tupleTypes.set(data.id, data.isTupleType === true); + return this.type(references.typeData(data, this.id)); + } + + /** Whether a type a TypeScript 7.1 server reported is a tuple type. */ + isTupleType71(type: number): boolean { + const result = this.tupleTypes.get(type); + if (result === undefined) throw new Error(`Unknown TypeScript 7.1 type ${type}`); + return result; + } + + /** Whether the server speaks TypeScript 7.1's protocol. */ + ts71(): boolean { + return this.snapshot.references !== undefined; + } + + /** A symbol a TypeScript 7.1 reply holds. */ + symbol71(data: Ts71SymbolData): SemanticSymbol { + const references = this.snapshot.references; + if (references === undefined) throw new Error("TypeScript 7.1 symbol from a 7.0 server"); + return this.symbol(references.symbolData(data, this.id)); + } + + /** The local id of a symbol id a TypeScript 7.1 server reports as a plain + * number (getWellKnownSymbols): a symbol of this project's snapshot. */ + snapshotSymbolId(id: number): number { + const references = this.snapshot.references; + return references === undefined ? id : references.fromCompact({ id }, this.id); } /** A reply of one type, or undefined for null. */ requestType(method: string, query: SemanticQuery): SemanticType | undefined { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticType | undefined => this.types.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71TypeData): SemanticType => this.type71(data); + return decodeSemanticObject(bytes, cached, build71) ?? undefined; + } const build = (data: TypeResponse): SemanticType => this.type(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; } @@ -176,6 +460,11 @@ export class SemanticProject { requestTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticType | undefined => this.types.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71TypeData): SemanticType => this.type71(data); + return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined); + } const build = (data: TypeResponse): SemanticType => this.type(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); } @@ -183,6 +472,14 @@ export class SemanticProject { /** A reply of one symbol, or undefined for null. */ requestSymbol(method: string, query: SemanticQuery): SemanticSymbol | undefined { const bytes = this.requestBinary(method, query); + const references = this.snapshot.references; + if (references !== undefined) { + // TypeScript 7.1's symbols start with their reference, not an id. + const none = (): SemanticSymbol | undefined => undefined; + const build71 = (data: Ts71SymbolData): SemanticSymbol => + this.symbol(references.symbolData(data, this.id)); + return decodeSemanticObject(bytes, none, build71) ?? undefined; + } const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; @@ -191,6 +488,13 @@ export class SemanticProject { /** A reply of symbols and nulls (undefined here), or [] for null. */ requestSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] { const bytes = this.requestBinary(method, query); + const references = this.snapshot.references; + if (references !== undefined) { + const none = (): SemanticSymbol | undefined => undefined; + const build71 = (data: Ts71SymbolData): SemanticSymbol => + this.symbol(references.symbolData(data, this.id)); + return (decodeSemanticObjects(bytes, none, build71) ?? []).map((item) => item ?? undefined); + } const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); @@ -200,6 +504,12 @@ export class SemanticProject { requestSignature(method: string, query: SemanticQuery): SemanticSignature | undefined { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71SignatureData): SemanticSignature => + this.signature(references.signatureData(data, this.id)); + return decodeSemanticObject(bytes, cached, build71) ?? undefined; + } const build = (data: SignatureResponse): SemanticSignature => this.signature(data); return decodeSemanticObject(bytes, cached, build) ?? undefined; } @@ -208,6 +518,12 @@ export class SemanticProject { requestSignatures(method: string, query: SemanticQuery): (SemanticSignature | undefined)[] { const bytes = this.requestBinary(method, query); const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const references = this.snapshot.references; + if (references !== undefined) { + const build71 = (data: Ts71SignatureData): SemanticSignature => + this.signature(references.signatureData(data, this.id)); + return (decodeSemanticObjects(bytes, cached, build71) ?? []).map((item) => item ?? undefined); + } const build = (data: SignatureResponse): SemanticSignature => this.signature(data); return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); } diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index d957fe0dd..5bf50a087 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -11,7 +11,7 @@ import { type Ts7FileSystem, } from "./rpc-filesystem.js"; import { spawnTs7Wire } from "./rpc-process.js"; -import { tsgoExecutable } from "./rpc-api.js"; +import { ts7TestServers, type Ts7TestServer } from "./test-servers.js"; import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js"; import { AstNode } from "./ast-node.js"; import { ts7Diagnostic } from "./diagnostic.js"; @@ -27,10 +27,16 @@ const fallback: Ts7FileSystem = { getAccessibleEntries: () => undefined, }; -// These tests compare the session with the pinned SDK, which runs tsgo, so the session runs tsgo too. -function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { +// These tests compare the session with the pinned SDK, which runs tsgo 7.0. The session runs +// tsc 7.0 and tsc 7.1, whose answers to these cases are the same. +function transport( + server: Ts7TestServer, + cwd: string, + fs: Ts7FileSystem = fallback, + timing = false, +) { const rpc = new Ts7RpcClient( - spawnTs7Wire(tsgoExecutable(), [ + spawnTs7Wire(server.executable, [ "--api", "--cwd", cwd, @@ -38,265 +44,291 @@ function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { ...(timing ? ["--timing"] : []), ]), ); - registerTs7FileSystem(rpc, fs); + registerTs7FileSystem(rpc, fs, () => server.ts71); return rpc; } -function connect(cwd: string, fs: Ts7FileSystem = fallback, timing = false) { - return new Ts7Session(transport(cwd, fs, timing), timing); +function connect(server: Ts7TestServer, cwd: string, fs: Ts7FileSystem = fallback, timing = false) { + return new Ts7Session(transport(server, cwd, fs, timing), timing); } -test("owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - writeFileSync(file, 'export const answer: number = "wrong";\n'); - writeFileSync( - config, - JSON.stringify({ - compilerOptions: { - strict: true, - target: "esnext", - types: [], - declaration: true, - sourceMap: true, - paths: { "named/*": ["./*", "../*"] }, - }, - files: [file], - }), - ); - const session = connect(dir, fallback, true); - const sdk = new API({ cwd: dir }); - try { - expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config)); - const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] }); - const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] }); - expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot); - const project = snapshot.getProject(config)!; - const expected = oracle.getProject(config)!; - expect(project).toBeInstanceOf(Ts7SessionProject); - expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions()); - expect(project.rootFiles).toEqual(expected.rootFiles); - expect(snapshot.getDefaultProjectForFile(file)).toBe(project); - expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project); - expect(project.program.getSourceFileNames()).toEqual(expected.program.getSourceFileNames()); - const source = project.program.getSourceFile(file)!; - expect(source).toBeInstanceOf(AstNode); - expect(source.text).toBe(expected.program.getSourceFile(file)!.text); - const metadata = project.program.getSourceFileMetadata(file)!; - expect(metadata).toEqual(expected.program.getSourceFileMetadata(file)); - expect(project.program.getSourceFileMetadata(file)).toBe(metadata); - const missing = protocolPath(join(dir, "missing.ts")); - expect(project.program.getSourceFile(missing)).toBeUndefined(); - const sourceRequests = session.getTimingInfo().totals.requestCount; - expect(project.program.getSourceFile(file)).toBe(source); - expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe( - source, +for (const server of ts7TestServers()) + test(`owned snapshots preserve SDK options, diagnostics, metadata and default-project lookup (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-parity-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + writeFileSync(file, 'export const answer: number = "wrong";\n'); + writeFileSync( + config, + JSON.stringify({ + compilerOptions: { + strict: true, + target: "esnext", + types: [], + declaration: true, + sourceMap: true, + paths: { "named/*": ["./*", "../*"] }, + }, + files: [file], + }), ); - expect(project.program.getSourceFile(missing)).toBeUndefined(); - expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests); - expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); - const before = session.getTimingInfo().totals.requestCount; - expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); - expect(session.getTimingInfo().totals.requestCount).toBe(before); - for (const name of [ - "getSyntacticDiagnostics", - "getBindDiagnostics", - "getSemanticDiagnostics", - "getSuggestionDiagnostics", - "getDeclarationDiagnostics", - ] as const) { - // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. - expect(project.program[name](file), name).toEqual( - expected.program[name](file).map(ts7Diagnostic), + const session = connect(server, dir, fallback, true); + const sdk = new API({ cwd: dir }); + try { + expect(session.parseConfigFile(config)).toEqual(sdk.parseConfigFile(config)); + const snapshot = session.updateSnapshot({ openProjects: [config], openFiles: [file] }); + const oracle = sdk.updateSnapshot({ openProjects: [config], openFiles: [file] }); + expect(snapshot).toBeInstanceOf(Ts7SessionSnapshot); + const project = snapshot.getProject(config)!; + const expected = oracle.getProject(config)!; + expect(project).toBeInstanceOf(Ts7SessionProject); + expect(project.program.getCompilerOptions()).toEqual(expected.program.getCompilerOptions()); + expect(project.rootFiles).toEqual(expected.rootFiles); + expect(snapshot.getDefaultProjectForFile(file)).toBe(project); + expect(snapshot.getProject(protocolPath(join(dir, "./tsconfig.json")))).toBe(project); + const names = project.program.getSourceFileNames(); + const expectedNames = expected.program.getSourceFileNames(); + if (server.ts71) { + // tsc 7.1 reads its own lib files, which split some esnext libs into es2026 ones. + const isLib = (name: string) => /\/lib\.[^/]*\.d\.ts$/.test(name); + expect(names.filter((name) => !isLib(name))).toEqual( + expectedNames.filter((name) => !isLib(name)), + ); + for (const lib of ["lib.es5.d.ts", "lib.esnext.d.ts"]) + expect( + names.some((name) => name.endsWith(`/${lib}`)), + lib, + ).toBe(true); + } else { + expect(names).toEqual(expectedNames); + } + const source = project.program.getSourceFile(file)!; + expect(source).toBeInstanceOf(AstNode); + expect(source.text).toBe(expected.program.getSourceFile(file)!.text); + const metadata = project.program.getSourceFileMetadata(file)!; + expect(metadata).toEqual(expected.program.getSourceFileMetadata(file)); + expect(project.program.getSourceFileMetadata(file)).toBe(metadata); + const missing = protocolPath(join(dir, "missing.ts")); + expect(project.program.getSourceFile(missing)).toBeUndefined(); + const sourceRequests = session.getTimingInfo().totals.requestCount; + expect(project.program.getSourceFile(file)).toBe(source); + expect(project.program.getSourceFile({ uri: pathToFileURL(join(dir, "main.ts")).href })).toBe( + source, ); - expect(project.program[name](), name).toEqual(expected.program[name]().map(ts7Diagnostic)); - } - for (const name of [ - "getProgramDiagnostics", - "getGlobalDiagnostics", - "getConfigFileParsingDiagnostics", - ] as const) - expect(project.program[name](), name).toEqual(expected.program[name]()); - const library = project.program - .getSourceFileNames() - .find((name) => name.endsWith("lib.esnext.d.ts"))!; - expect(library).toBeDefined(); - expect( - project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!), - ).toBe(true); - expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false); - snapshot.dispose(); - expect(snapshot.isDisposed()).toBe(true); - expect(() => snapshot.getProjects()).toThrow("disposed"); - expect(() => project.program.getSourceFileNames()).toThrow("disposed"); - } finally { - session.close(); - sdk.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); - -test("virtual updates share active ASTs and refetch after releasing their baseline", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-")); - const a = protocolPath(join(dir, "a.json")), - b = protocolPath(join(dir, "b.json")); - const changed = protocolPath(join(dir, "changed.ts")), - stable = protocolPath(join(dir, "stable.ts")); - let content = "export const value = 1;"; - let option = "esnext"; - const overlay: Ts7FileSystem = { - ...fallback, - readFile: (file) => { - if (file === a || file === b) - return JSON.stringify({ - compilerOptions: { strict: true, target: option, types: [] }, - files: [changed, stable], - }); - if (file === changed) return content; - if (file === stable) return "export const stable = true;"; - return undefined; - }, - fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined), - }; - const session = connect(dir, overlay, true); - const sdk = new API({ cwd: dir, fs: overlay }); - try { - const first = session.updateSnapshot({ openProjects: [a, b] }); - const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] }); - const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!; - const old = first.getProject(a)!.program.getSourceFile(changed)!; - const retained = first.getProject(a)!.program.getSourceFile(stable)!; - expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old); - content = 'export const value: number = "wrong";'; - const second = session.updateSnapshot({ fileChanges: { changed: [changed] } }); - const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } }); - const next = second.getProject(a)!.program.getSourceFile(changed)!; - expect(next).not.toBe(old); - expect(old.text).toContain("= 1"); - expect(next.text).toBe(content); - expect( - second - .getProject(a)! - .program.getSemanticDiagnostics(changed) - .map((d) => d.code), - ).toEqual([2322]); - expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]); - expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained); - first.dispose(); - second.dispose(); - oracleFirst.dispose(); - oracleSecond.dispose(); - const before = session.getTimingInfo().totals.sourceFilesFetched; - const third = session.updateSnapshot(); - const oracleThird = sdk.updateSnapshot(); - const refreshed = third.getProject(a)!.program.getSourceFile(stable)!; - expect(refreshed.text).toBe(retained.text); - expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1); - option = "es2020"; - const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } }); - const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } }); - const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!; - expect(reparsed === refreshed).toBe( - oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable, - ); - expect(fourth.getProject(a)!.compilerOptions.target).toBe( - oracleFourth.getProject(a)!.compilerOptions.target, - ); - expect(fourth.getProject(a)!.compilerOptions.target).not.toBe( - third.getProject(a)!.compilerOptions.target, - ); - expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable); - const closed = session.updateSnapshot({ closeProjects: [b] }); - expect(closed.getProject(b)).toBeUndefined(); - expect(closed.getProject(a)).toBeDefined(); - session.close(); - expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true); - expect(session.cache.size).toBe(0); - expect(() => session.updateSnapshot()).toThrow("closed"); - expect(() => session.parseConfigFile(a)).toThrow("closed"); - expect(() => session.getTimingInfo()).toThrow("closed"); - } finally { - session.close(); - sdk.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); - -test("transport failure during close seals every snapshot and clears retained ASTs", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - writeFileSync(file, "export const answer = 42;"); - writeFileSync( - config, - JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }), - ); - const rpc = transport(dir); - const session = new Ts7Session(rpc); - try { - const first = session.updateSnapshot({ openProjects: [config] }); - const project = first.getProject(config)!; - const source = project.program.getSourceFile(file)!; - const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!; - const second = session.updateSnapshot(); - const secondProject = second.getProject(config)!; - expect(secondProject.program.getSourceFile(file)).toBe(source); - const third = session.updateSnapshot(); - expect(session.cache.size).toBe(1); - rpc.close(); - expect(() => session.close()).toThrow("closed"); - for (const snapshot of [first, second, third]) { + expect(project.program.getSourceFile(missing)).toBeUndefined(); + expect(session.getTimingInfo().totals.requestCount).toBe(sourceRequests); + expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); + const before = session.getTimingInfo().totals.requestCount; + expect(project.program.getSourceFileMetadata(missing)).toBeUndefined(); + expect(session.getTimingInfo().totals.requestCount).toBe(before); + for (const name of [ + "getSyntacticDiagnostics", + "getBindDiagnostics", + "getSemanticDiagnostics", + "getSuggestionDiagnostics", + "getDeclarationDiagnostics", + ] as const) { + // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. + expect(project.program[name](file), name).toEqual( + expected.program[name](file).map(ts7Diagnostic), + ); + // tsc 7.1's lib files differ from 7.0's, and so do their diagnostics. + const own = (list: T[]) => + server.ts71 + ? list.filter((item) => !/\/lib\.[^/]*\.d\.ts$/.test(item.fileName ?? "")) + : list; + expect(own(project.program[name]()), name).toEqual( + own(expected.program[name]().map(ts7Diagnostic)), + ); + } + for (const name of [ + "getProgramDiagnostics", + "getGlobalDiagnostics", + "getConfigFileParsingDiagnostics", + ] as const) + expect(project.program[name](), name).toEqual(expected.program[name]()); + const library = project.program + .getSourceFileNames() + .find((name) => name.endsWith("lib.esnext.d.ts"))!; + expect(library).toBeDefined(); + expect( + project.program.isSourceFileDefaultLibrary(project.program.getSourceFile(library)!), + ).toBe(true); + expect(project.program.isSourceFileFromExternalLibrary(source)).toBe(false); + snapshot.dispose(); expect(snapshot.isDisposed()).toBe(true); - expect(() => snapshot.getProjects()).toThrow("closed"); - expect(() => snapshot.dispose()).not.toThrow(); + expect(() => snapshot.getProjects()).toThrow("disposed"); + expect(() => project.program.getSourceFileNames()).toThrow("disposed"); + } finally { + session.close(); + sdk.close(); + rmSync(dir, { recursive: true, force: true }); } - expect(() => secondProject.checker.getNumberType()).toThrow("disposed"); - expect(() => secondProject.program.getSourceFile(file)).toThrow("closed"); - expect(() => type.getRegularType()).toThrow("disposed"); - expect(session.cache.size).toBe(0); - expect(() => session.close()).not.toThrow(); - } finally { - session.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); + }); -test("a released latest snapshot cannot lend stale ASTs to the same project", () => { - const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-")); - const file = protocolPath(join(dir, "main.ts")); - const config = protocolPath(join(dir, "tsconfig.json")); - let source = "export const value = 1;"; - const overlay: Ts7FileSystem = { - ...fallback, - readFile: (path) => - path === file - ? source - : path === config - ? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] }) - : null, - fileExists: (path) => path === file || path === config, - }; - const session = connect(dir, overlay); - try { - const first = session.updateSnapshot({ openProjects: [config] }); - const old = first.getProject(config)!.program.getSourceFile(file)!; - first.dispose(); - source = 'export const replacement = "new";'; - const second = session.updateSnapshot({ fileChanges: { changed: [file] } }); - const current = second.getProject(config)!.program.getSourceFile(file)!; - expect(current.text).toBe(source); - expect(current).not.toBe(old); - expect(old.text).toBe("export const value = 1;"); - second.dispose(); - const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } }); - deleted.dispose(); - source = "export const third = true;"; - const third = session.updateSnapshot({ fileChanges: { created: [file] } }); - expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); - } finally { - session.close(); - rmSync(dir, { recursive: true, force: true }); - } -}); +for (const server of ts7TestServers()) + test(`virtual updates share active ASTs and refetch after releasing their baseline (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-updates-")); + const a = protocolPath(join(dir, "a.json")), + b = protocolPath(join(dir, "b.json")); + const changed = protocolPath(join(dir, "changed.ts")), + stable = protocolPath(join(dir, "stable.ts")); + let content = "export const value = 1;"; + let option = "esnext"; + const overlay: Ts7FileSystem = { + ...fallback, + readFile: (file) => { + if (file === a || file === b) + return JSON.stringify({ + compilerOptions: { strict: true, target: option, types: [] }, + files: [changed, stable], + }); + if (file === changed) return content; + if (file === stable) return "export const stable = true;"; + return undefined; + }, + fileExists: (file) => ([a, b, changed, stable].includes(file) ? true : undefined), + }; + const session = connect(server, dir, overlay, true); + const sdk = new API({ cwd: dir, fs: overlay }); + try { + const first = session.updateSnapshot({ openProjects: [a, b] }); + const oracleFirst = sdk.updateSnapshot({ openProjects: [a, b] }); + const oracleStable = oracleFirst.getProject(a)!.program.getSourceFile(stable)!; + const old = first.getProject(a)!.program.getSourceFile(changed)!; + const retained = first.getProject(a)!.program.getSourceFile(stable)!; + expect(first.getProject(b)!.program.getSourceFile(changed)).toBe(old); + content = 'export const value: number = "wrong";'; + const second = session.updateSnapshot({ fileChanges: { changed: [changed] } }); + const oracleSecond = sdk.updateSnapshot({ fileChanges: { changed: [changed] } }); + const next = second.getProject(a)!.program.getSourceFile(changed)!; + expect(next).not.toBe(old); + expect(old.text).toContain("= 1"); + expect(next.text).toBe(content); + expect( + second + .getProject(a)! + .program.getSemanticDiagnostics(changed) + .map((d) => d.code), + ).toEqual([2322]); + expect(first.getProject(a)!.program.getSemanticDiagnostics(changed)).toEqual([]); + expect(second.getProject(a)!.program.getSourceFile(stable)).toBe(retained); + first.dispose(); + second.dispose(); + oracleFirst.dispose(); + oracleSecond.dispose(); + const before = session.getTimingInfo().totals.sourceFilesFetched; + const third = session.updateSnapshot(); + const oracleThird = sdk.updateSnapshot(); + const refreshed = third.getProject(a)!.program.getSourceFile(stable)!; + expect(refreshed.text).toBe(retained.text); + expect(session.getTimingInfo().totals.sourceFilesFetched).toBe(before + 1); + option = "es2020"; + const fourth = session.updateSnapshot({ fileChanges: { changed: [a, b] } }); + const oracleFourth = sdk.updateSnapshot({ fileChanges: { changed: [a, b] } }); + const reparsed = fourth.getProject(a)!.program.getSourceFile(stable)!; + expect(reparsed === refreshed).toBe( + oracleFourth.getProject(a)!.program.getSourceFile(stable) === oracleStable, + ); + expect(fourth.getProject(a)!.compilerOptions.target).toBe( + oracleFourth.getProject(a)!.compilerOptions.target, + ); + expect(fourth.getProject(a)!.compilerOptions.target).not.toBe( + third.getProject(a)!.compilerOptions.target, + ); + expect(oracleThird.getProject(a)!.program.getSourceFile(stable)).toBe(oracleStable); + const closed = session.updateSnapshot({ closeProjects: [b] }); + expect(closed.getProject(b)).toBeUndefined(); + expect(closed.getProject(a)).toBeDefined(); + session.close(); + expect(third.isDisposed() && fourth.isDisposed() && closed.isDisposed()).toBe(true); + expect(session.cache.size).toBe(0); + expect(() => session.updateSnapshot()).toThrow("closed"); + expect(() => session.parseConfigFile(a)).toThrow("closed"); + expect(() => session.getTimingInfo()).toThrow("closed"); + } finally { + session.close(); + sdk.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); + +for (const server of ts7TestServers()) + test(`transport failure during close seals every snapshot and clears retained ASTs (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-close-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + writeFileSync(file, "export const answer = 42;"); + writeFileSync( + config, + JSON.stringify({ compilerOptions: { strict: true, types: [] }, files: [file] }), + ); + const rpc = transport(server, dir); + const session = new Ts7Session(rpc); + try { + const first = session.updateSnapshot({ openProjects: [config] }); + const project = first.getProject(config)!; + const source = project.program.getSourceFile(file)!; + const type = project.checker.getTypeAtPosition(file, source.text!.indexOf("answer"))!; + const second = session.updateSnapshot(); + const secondProject = second.getProject(config)!; + expect(secondProject.program.getSourceFile(file)).toBe(source); + const third = session.updateSnapshot(); + expect(session.cache.size).toBe(1); + rpc.close(); + expect(() => session.close()).toThrow("closed"); + for (const snapshot of [first, second, third]) { + expect(snapshot.isDisposed()).toBe(true); + expect(() => snapshot.getProjects()).toThrow("closed"); + expect(() => snapshot.dispose()).not.toThrow(); + } + expect(() => secondProject.checker.getNumberType()).toThrow("disposed"); + expect(() => secondProject.program.getSourceFile(file)).toThrow("closed"); + expect(() => type.getRegularType()).toThrow("disposed"); + expect(session.cache.size).toBe(0); + expect(() => session.close()).not.toThrow(); + } finally { + session.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); + +for (const server of ts7TestServers()) + test(`a released latest snapshot cannot lend stale ASTs to the same project (${server.name})`, () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-session-released-")); + const file = protocolPath(join(dir, "main.ts")); + const config = protocolPath(join(dir, "tsconfig.json")); + let source = "export const value = 1;"; + const overlay: Ts7FileSystem = { + ...fallback, + readFile: (path) => + path === file + ? source + : path === config + ? JSON.stringify({ compilerOptions: { noLib: true, types: [] }, files: [file] }) + : null, + fileExists: (path) => path === file || path === config, + }; + const session = connect(server, dir, overlay); + try { + const first = session.updateSnapshot({ openProjects: [config] }); + const old = first.getProject(config)!.program.getSourceFile(file)!; + first.dispose(); + source = 'export const replacement = "new";'; + const second = session.updateSnapshot({ fileChanges: { changed: [file] } }); + const current = second.getProject(config)!.program.getSourceFile(file)!; + expect(current.text).toBe(source); + expect(current).not.toBe(old); + expect(old.text).toBe("export const value = 1;"); + second.dispose(); + const deleted = session.updateSnapshot({ fileChanges: { deleted: [file] } }); + deleted.dispose(); + source = "export const third = true;"; + const third = session.updateSnapshot({ fileChanges: { created: [file] } }); + expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); + expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); + } finally { + session.close(); + rmSync(dir, { recursive: true, force: true }); + } + }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 082bb79a6..9223c897f 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -12,7 +12,7 @@ import type { Ts7CompilerOptionsData, Ts7ConfigData, Ts7DiagnosticData, - Ts7InitializeData, + Ts7ProjectChangeData, Ts7ProjectData, Ts7SnapshotData, Ts7SourceMetadata, @@ -36,6 +36,52 @@ interface Ts7ReportedSnapshotData extends Ts7SnapshotData { projects: Ts7ReportedProjectData[]; } +/** initialize's answer. TypeScript 7.0 reports useCaseSensitiveFileNames; + * 7.1 reports caseSensitivity (1 when case-sensitive) instead, which is how + * a session tells the two protocols apart. */ +interface Ts7InitializeReply { + currentDirectory: string; + useCaseSensitiveFileNames?: boolean; + caseSensitivity?: number; +} + +/** A project as TypeScript 7.1 reports it: the parsed command line carries + * the root files and options, and a project need not have a config file. */ +interface Ts71ProjectData { + id: string; + configFileName?: string; + parsedCommandLine: { fileNames: string[]; options: Ts7ProjectCompilerOptions }; +} + +/** createSnapshot's and updateSnapshot's answer in TypeScript 7.1. An update + * lists only the projects added or replaced since its base snapshot, and a + * changed project's entry is null when its files are not listed. */ +interface Ts71SnapshotData { + snapshot: number; + projects: Ts71ProjectData[]; + changes?: { + changedProjects?: Record; + removedProjects?: string[]; + }; +} + +/** TypeScript 7.1's compiler options in 7.0's values: 7.1 swaps JsxEmit's + * React (2) and ReactNative (3). */ +function options71(options: T): T { + if (options.jsx === 2) options.jsx = 3; + else if (options.jsx === 3) options.jsx = 2; + return options; +} + +function project71(data: Ts71ProjectData): Ts7ReportedProjectData { + return { + id: data.id, + configFileName: data.configFileName ?? data.id, + compilerOptions: options71(data.parsedCommandLine.options), + rootFiles: data.parsedCommandLine.fileNames, + }; +} + export interface Ts7FileChanges { changed?: SemanticDocument[]; created?: SemanticDocument[]; @@ -55,7 +101,7 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; - files?: string[]; + files?: SemanticDocument[]; } /** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ @@ -73,6 +119,8 @@ export class Ts7Session { private readonly snapshots = new Set(); private latest: Ts7SessionSnapshot | undefined; private paths: Ts7Paths | undefined; + /** Whether the server speaks TypeScript 7.1's protocol (initialize). */ + private ts71 = false; private closed = false; constructor( @@ -116,22 +164,41 @@ export class Ts7Session { initialize(): Ts7Paths { this.ensureOpen(); if (this.paths === undefined) { - const data = parseSemanticJson(this.text("initialize", "null")); - this.paths = new Ts7Paths(data.currentDirectory, data.useCaseSensitiveFileNames); + const data = parseSemanticJson(this.text("initialize", "null")); + this.ts71 = data.caseSensitivity !== undefined; + this.paths = new Ts7Paths( + data.currentDirectory, + this.ts71 ? data.caseSensitivity === 1 : data.useCaseSensitiveFileNames === true, + ); } return this.paths; } parseConfigFile(file: SemanticDocument): Ts7ConfigData { this.initialize(); - return this.request("parseConfigFile", { file }); + const data = this.request("parseConfigFile", { file }); + // TypeScript 7.1 also reports the raw config, errors and compileOnSave. + return this.ts71 ? { options: options71(data.options), fileNames: data.fileNames } : data; + } + + /** Whether initialize found a TypeScript 7.1 server; false before it. */ + speaksTs71(): boolean { + return this.ts71; + } + + /** Whether the server speaks TypeScript 7.1's protocol. */ + usesTs71Protocol(): boolean { + this.initialize(); + return this.ts71; } updateSnapshot(params: Ts7Update = {}): Ts7SessionSnapshot { const paths = this.initialize(); - const data = parseSemanticJson( - this.text("updateSnapshot", JSON.stringify(params)), - ); + const data = this.ts71 + ? this.snapshot71(params) + : parseSemanticJson( + this.text("updateSnapshot", JSON.stringify(params)), + ); const snapshot = new Ts7SessionSnapshot(data, this, paths); const previous = this.latest; if (previous !== undefined) { @@ -147,9 +214,56 @@ export class Ts7Session { return snapshot; } + /** TypeScript 7.1 derives a snapshot from a base snapshot the client + * names (updateSnapshot), or creates the first one (createSnapshot). The + * base is the latest snapshot: the server keeps it until the next update, + * even after the client disposes it (release), so open projects and files + * carry over as they do on a 7.0 server. An update lists only the projects + * that changed; the others carry over from the base. */ + private snapshot71(params: Ts7Update): Ts7ReportedSnapshotData { + const changes: Record = {}; + if (params.openProjects !== undefined) changes["openProjects"] = params.openProjects; + if (params.closeProjects !== undefined) changes["closeProjects"] = params.closeProjects; + if (params.openFiles !== undefined) changes["openFiles"] = params.openFiles; + if (params.closeFiles !== undefined) changes["closeFiles"] = params.closeFiles; + if (params.fileChanges !== undefined) changes["fileNotifications"] = params.fileChanges; + // A 7.1 snapshot leaves a project whose files changed dirty until asked + // to update its program; a 7.0 snapshot's programs are always current. + changes["ensurePrograms"] = true; + const base = this.latest; + const data = parseSemanticJson( + base === undefined + ? this.text("createSnapshot", JSON.stringify(changes)) + : this.text("updateSnapshot", JSON.stringify({ snapshot: base.id, changes })), + ); + if (base !== undefined && base.isDisposed()) + this.binary("release", JSON.stringify({ snapshot: base.id })); + const projects = new Map(); + const removed = new Set(data.changes?.removedProjects ?? []); + if (base !== undefined) + for (const project of base.projectData) + if (!removed.has(project.id)) projects.set(project.id, project); + for (const project of data.projects) projects.set(project.id, project71(project)); + // A changed project without a file list keeps none of its cached files. + const changedProjects: Record = {}; + const removedProjects = [...removed]; + for (const [id, change] of Object.entries(data.changes?.changedProjects ?? {})) { + if (change === null) removedProjects.push(id); + else changedProjects[id] = change; + } + return { + snapshot: data.snapshot, + projects: [...projects.values()], + changes: { changedProjects, removedProjects }, + }; + } + release(snapshot: Ts7SessionSnapshot): void { this.snapshots.delete(snapshot); if (snapshot !== this.latest) this.cache.release(snapshot.id); + // A 7.1 server keeps the latest snapshot as the next update's base + // (snapshot71), which releases it after deriving from it. + if (this.ts71 && snapshot === this.latest) return; this.binary("release", JSON.stringify({ snapshot: snapshot.id })); } @@ -180,6 +294,8 @@ export class Ts7Session { export class Ts7SessionSnapshot { readonly id: number; + /** The projects as the server reported them (TypeScript 7.0's shape). */ + readonly projectData: readonly Ts7ReportedProjectData[]; readonly semantic: SemanticSnapshot; private readonly projects = new Map(); private disposed = false; @@ -190,10 +306,15 @@ export class Ts7SessionSnapshot { readonly paths: Ts7Paths, ) { this.id = data.snapshot; - this.semantic = new SemanticSnapshot(this.id, { - text: (method, payload) => this.session.text(method, payload), - binary: (method, payload) => this.session.binary(method, payload), - }); + this.projectData = data.projects; + this.semantic = new SemanticSnapshot( + this.id, + { + text: (method, payload) => this.session.text(method, payload), + binary: (method, payload) => this.session.binary(method, payload), + }, + session.speaksTs71(), + ); for (const item of data.projects) { const project = new Ts7SessionProject(item, this, session); this.projects.set(paths.canonical(item.configFileName), project); @@ -214,10 +335,15 @@ export class Ts7SessionSnapshot { } getDefaultProjectForFile(file: SemanticDocument): Ts7SessionProject | undefined { this.ensureActive(); - const data = this.session.request("getDefaultProjectForFile", { - snapshot: this.id, - file, - }); + const query = { snapshot: this.id, file }; + if (this.session.usesTs71Protocol()) { + const data = this.session.request("getDefaultProjectForFile", query); + return data === null ? undefined : this.getProject(project71(data).configFileName); + } + const data = this.session.request( + "getDefaultProjectForFile", + query, + ); return data === null ? undefined : this.getProject(data.configFileName); } isDisposed(): boolean { @@ -344,17 +470,29 @@ export class Ts7SessionProgram { isSourceFileDefaultLibrary(file: AstNode): boolean { return this.getSourceFileMetadataByPath(file.path)?.isDefaultLibrary ?? false; } + /** A diagnostics request for `file`, or for the whole program. TypeScript + * 7.1 takes the file in a list (`files`). */ + private diagnosticsQuery(file?: SemanticDocument): ProgramQuery { + if (file === undefined || !this.session.usesTs71Protocol()) return this.query(file); + const query = this.query(); + query.files = [file]; + return query; + } private diagnostics(method: string, file?: SemanticDocument): Ts7Diagnostic[] { const result: Ts7Diagnostic[] = []; - for (const data of this.session.request(method, this.query(file)) ?? - []) + for (const data of this.session.request( + method, + this.diagnosticsQuery(file), + ) ?? []) 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)) ?? []; + return ( + this.session.request(method, this.diagnosticsQuery(file)) ?? [] + ); } /** The semantic diagnostics of each of `files` and the global diagnostics, * after the server checks only those files (getDiagnosticsOfCheckedFiles), diff --git a/packages/compiler/src/frontend/ts7/test-servers.ts b/packages/compiler/src/frontend/ts7/test-servers.ts new file mode 100644 index 000000000..68e1e4f0c --- /dev/null +++ b/packages/compiler/src/frontend/ts7/test-servers.ts @@ -0,0 +1,28 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { tsgoExecutable } from "./rpc-api.js"; + +/** An API server the TypeScript client tests run against. */ +export interface Ts7TestServer { + /** The label tests append to their names. */ + name: string; + executable: string; + /** Whether the server speaks TypeScript 7.1's protocol. */ + ts71: boolean; +} + +const require = createRequire(import.meta.url); + +/** tsc 7.0.2 from the pinned `typescript` package and tsc 7.1.0-beta from + * the `typescript-7.1` test dependency, so the client's protocol 5 and + * protocol 9 paths answer the same cases. For tests only. */ +export function ts7TestServers(): Ts7TestServer[] { + const root = dirname(require.resolve("typescript-7.1/package.json")); + const { default: getExePath } = require(join(root, "lib/getExePath.js")) as { + default: () => string; + }; + return [ + { name: "tsc 7.0", executable: tsgoExecutable(), ts71: false }, + { name: "tsc 7.1", executable: getExePath(), ts71: true }, + ]; +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 0635ad6ae..40ca10b38 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1282,6 +1282,23 @@ export interface TypeMapperCtx { * 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; + /** Localized failure's lenient mapping (Lowerer.lenientType): a nested + * component whose mapping fails maps to the checked dynamic instead, so a + * failing type keeps its outer shape with dynamic pieces. Only ever set + * on a throwaway context without memoization. */ + lenient?: boolean; + /** With `lenient`: told each component the lenient mapping turned into + * the checked dynamic (Lowerer.lenientPieces). */ + onLenientPiece?: (type: ts.Type) => void; + /** 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 @@ -1344,7 +1361,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 @@ -1805,7 +1822,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 @@ -1827,8 +1885,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; @@ -1856,6 +1914,23 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { 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++; + } + } + // Push-down (stage L3): in a lenient mapping, the smallest failing + // component is the checked dynamic. + if (mapped === null && ctx.lenient === true && !topLevel) { + mapped = DYN; + contextResolutions++; + ctx.onLenientPiece?.(type); + } if ( mapped !== null && memoKey !== null && @@ -2600,6 +2675,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++; 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 9d73d8209..a41a6b4c7 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,3 +1,4 @@ +import { shapeOverlayEnabled } from "./frontend/shape-inference/index.js"; import { nodeFrontendServices } from "./frontend/services-node.js"; import { runTypePatchCommand, @@ -54,6 +55,7 @@ export type { 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"; @@ -63,6 +65,7 @@ import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node: 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,9 +95,17 @@ import { NativeCodegenError, writeLlvmSource, } from "./backend/native-codegen.js"; -import { nativePartitionPaths, nativeProgramPartitions } from "./backend/native-codegen-core.js"; +import { + nativeCodegenParts, + nativePartitionPaths, + nativeProgramPartitions, +} from "./backend/native-codegen-core.js"; import { privateSiblingPath } from "./backend/build-cache.js"; -import { nativeCodegenTarget, nativeCodegenTargetRefusal } from "./backend/targets.js"; +import { + nativeCodegenTarget, + nativeCodegenTargetRefusal, + staticLinkRefusal, +} from "./backend/targets.js"; import { windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./backend/targets.js"; import { createNativeLinkInfo } from "./backend/native-link-info.js"; import { RuntimePackError } from "./backend/runtime-pack.js"; @@ -154,6 +165,8 @@ import { clearResolveCaches } from "./frontend/resolve.js"; import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js"; import { executableLinkFeatures } from "./backend/executable-features.js"; import { youngNurseryRequested } from "./backend/young-nursery.js"; +import { heapCensusRequested } from "./backend/llvm/heap-census.js"; +import { representationReportPath } from "./backend/llvm/repr-report.js"; import { type FrontendInputSnapshot, FrontendInputTracker, @@ -479,6 +492,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. @@ -491,6 +508,10 @@ export async function compile( } 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; + } } } @@ -529,6 +550,14 @@ function performanceWarnings(sink: CompileWarningSink | undefined): { 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, @@ -564,11 +593,12 @@ function executableNativeFeatures( mod: IrModule, backend: "llvm", optimization: NativeOptimization, + largePages: boolean, ): EarlyExecutableNativeFeatures { return { backend, ...optimizationField(optimization), - ...executableLinkFeatures(mod), + ...executableLinkFeatures(mod, largePages), }; } @@ -580,6 +610,7 @@ async function compileExecutableNative( ffi: FfiProfile | null, windowsSubsystem?: WindowsSubsystem, strip?: boolean, + staticLink = false, programSplit: ReturnType = null, programObjectDependencies: readonly NativeArtifactDependency[] = [], onArtifactReady?: NonNullable[0]["onArtifactReady"]>, @@ -601,6 +632,7 @@ async function compileExecutableNative( ffi, optimization: features.optimization ?? "release", ...(strip ? { strip: true } : {}), + ...(staticLink ? { staticLink: true } : {}), ...(windowsSubsystem === undefined ? {} : { windowsSubsystem }), ...(profileGenerate ? { profileGenerate: true } : {}), programObjectDependencies, @@ -626,6 +658,8 @@ async function compileExecutableNative( : null); if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); + if (staticLink) + throw new InternalCompilerError("--static executables link against a precompiled runtime pack"); await compileExternalC({ ...(llvmVisiblePath === undefined || llvmVisiblePath === llvmPath ? { cPath: llvmPath } @@ -667,6 +701,7 @@ async function compileExecutableNative( ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.mime ? { mime: true } : {}), + ...(features.largePages ? { largePages: true } : {}), ...(features.utilTiming ? { utilTiming: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), @@ -739,8 +774,11 @@ async function emitNativeProgramObject( // The validated program-object artifact is a single object by contract. const target = nativeCodegenTarget(); const llvmBytes = llvmInputBytes(llvm); - const partitions = - opts.nativeProgramObject === true || target === null ? 1 : nativeProgramPartitions(llvmBytes); + const single = opts.nativeProgramObject === true || target === null; + // An optimized program that stays one partition is optimized whole and + // generates code for its parts concurrently. + const codegenParts = single || opts.optimization === "dev" ? 1 : nativeCodegenParts(llvmBytes); + const partitions = single ? 1 : Math.max(nativeProgramPartitions(llvmBytes), codegenParts); try { const artifact = await emitNativeArtifact({ outputPath: linkPath, @@ -750,6 +788,7 @@ async function emitNativeProgramObject( optimization: opts.optimization === "dev" ? "0" : "2", ...(opts.sanitize === undefined ? {} : { sanitize: opts.sanitize }), partitions, + ...(codegenParts > 1 ? { splitCodegen: true } : {}), ...(profile === undefined ? {} : { profile }), }); return { @@ -793,6 +832,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; @@ -866,6 +936,7 @@ 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", @@ -918,7 +989,12 @@ async function prepareExecutableInput( nativeLinkInfo: await createNativeLinkInfo({ programObject: opts.outPath, target, - features: executableNativeFeatures(mod, "llvm", opts.optimization ?? "release"), + features: executableNativeFeatures( + mod, + "llvm", + opts.optimization ?? "release", + opts.largePages === true, + ), ffi, optimization: opts.optimization ?? "release", }), @@ -940,6 +1016,7 @@ 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, @@ -968,7 +1045,12 @@ async function prepareExecutableInput( await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots())); } - const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); + const nativeFeatures = executableNativeFeatures( + mod, + backend, + opts.optimization ?? "release", + opts.largePages === true, + ); timing("link-features"); const programSplit = !useRuntimePack && @@ -1260,6 +1342,8 @@ function configureLibDiagnostics(root: string | null): void { 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. @@ -1383,6 +1467,19 @@ async function compileTracked( sourceTexts: new Map(), }; } + if (opts.largePages === true && outputKind !== "exe") { + return { + ok: false, + diagnostics: [ + nativeCodegenDiag( + "SC3002", + "--large-pages is only supported for executable output", + entryPath, + ), + ], + sourceTexts: new Map(), + }; + } if (opts.cpuDispatch === true && outputKind !== "exe") { return { ok: false, @@ -1544,6 +1641,19 @@ async function compileTracked( sourceTexts: new Map(), }; } + const staticRefusal = staticLinkRefusal({ + staticLink: opts.staticLink, + outputKind, + sanitize: opts.sanitize ?? false, + target: nativeCodegenTarget(), + }); + if (staticRefusal !== null) { + return { + ok: false, + diagnostics: [nativeCodegenDiag("SC3002", staticRefusal, entryPath)], + sourceTexts: new Map(), + }; + } try { windowsSubsystemLinkerArgs(buildPlatform, opts.windowsSubsystem); } catch (err) { @@ -1563,9 +1673,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, @@ -1596,14 +1708,19 @@ async function compileTracked( backend: "llvm", ...optimizationField(opts.optimization), ...(opts.strip ? { strip: true as const } : {}), + ...(opts.staticLink === true ? { staticLink: true as const } : {}), ...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}), ...(opts.detachedExit === true ? { detachedExit: true as const } : {}), + ...(opts.largePages === true ? { largePages: true as const } : {}), ...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 } : {}), + ...(heapCensusRequested() ? { heapCensus: true as const } : {}), npmStatic: opts.npmStatic ?? null, + ...(shapeOverlayEnabled() ? { shapeOverlay: true as const } : {}), ffiProfile: opts.ffiProfilePath === undefined || ffiProfileBytes === null ? null @@ -1645,8 +1762,9 @@ async function compileTracked( implementationDependencies: implementation.dependencies, }; } + // A cache hit emits no module, so it would write no representation report. const earlyHit = - earlyCacheOptions === null + earlyCacheOptions === null || representationReportPath() !== null ? null : await readEarlyExecutableCache(cacheRoot, earlyCacheOptions); if (earlyHit !== null) { @@ -1675,6 +1793,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, @@ -1738,6 +1857,7 @@ async function compileTracked( ffi, opts.windowsSubsystem, opts.strip, + opts.staticLink === true, null, nativeProgramObject?.dependencies, opts.nativeProgramObject === true @@ -1752,6 +1872,7 @@ async function compileTracked( executableRestored: true, nativeDependencies: dependencies, frontend: earlyHit.frontend, + ...profileMatchField(nativeProgramObject), }); }, nativeProgramObject?.partitionPaths, @@ -1778,6 +1899,13 @@ async function compileTracked( sourceTexts: new Map(), }; } + if (err instanceof NativeCodegenError) { + return { + ok: false, + diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], + sourceTexts: new Map(), + }; + } throw err; } finally { if (nativeProgramObject !== null) { @@ -1797,6 +1925,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: earlyHit.llvmPath, backend: earlyHit.native.backend, + ...profileMatchField(nativeProgramObject), }, binaryPath: opts.outPath, llvmPath: earlyHit.llvmPath, @@ -1875,6 +2004,7 @@ async function compileTracked( ffi, opts.windowsSubsystem, opts.strip, + opts.staticLink === true, programSplit, nativeProgramObject?.dependencies, opts.nativeProgramObject === true @@ -1888,6 +2018,7 @@ async function compileTracked( frontend: frontendInputs.snapshot(), ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), + ...profileMatchField(nativeProgramObject), }); publishedExecutable = true; }, @@ -1907,6 +2038,7 @@ async function compileTracked( frontend: frontendInputs.snapshot(), ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), + ...profileMatchField(nativeProgramObject), }).catch(() => undefined); } } catch (err) { @@ -1922,6 +2054,13 @@ async function compileTracked( sourceTexts, }; } + if (err instanceof NativeCodegenError) { + return { + ok: false, + diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], + sourceTexts, + }; + } throw err; } finally { if (nativeProgramObject !== null) { @@ -1935,7 +2074,6 @@ async function compileTracked( } await pruneBuildCache(cacheRoot); timing("complete"); - const profileMatch = nativeProgramObject?.profileMatch; return { ok: true, artifact: { @@ -1943,7 +2081,7 @@ async function compileTracked( path: opts.outPath, translationUnitPath: llvmPath, backend, - ...(profileMatch === undefined ? {} : { profileMatch }), + ...profileMatchField(nativeProgramObject), }, binaryPath: opts.outPath, llvmPath, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 423219efa..a0d75c864 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -765,6 +765,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.rmSync", "fs.rmdirSync", "fs.readdirSync", + "fs.readdirRecursiveSync", "fs.readdirTypesSync", "fs.globSync", "fs.globTypesSync", @@ -943,6 +944,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. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ca7cf9ebb..e50e61a92 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -592,6 +592,9 @@ function buildLibFnSignatures() { "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 @@ -1551,6 +1554,15 @@ function buildLibFnSignatures() { 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 @@ -3733,6 +3745,15 @@ function buildLibFnSignatures() { "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 @@ -3997,6 +4018,12 @@ function buildLibFnSignatures() { * 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 @@ -4031,6 +4058,32 @@ function buildLibFnSignatures() { // 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. diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index fc1d5873b..be1eae2c3 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -8,6 +8,7 @@ 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" }; @@ -302,26 +303,24 @@ function rewriteFunction(fn: IrFunction): IrFunction { const copyOf = new Map(); // Whether a candidate string is declared from a charAt. let charAtDeclared = false; - const collect = (value: unknown): void => { - if (Array.isArray(value)) for (const item of value) collect(item); - else if (value !== null && typeof value === "object") { - const node = value as Record; - if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) { - const id = node.localId as string; - const init = node.init as IrExpr; - 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); - } + // 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); } - for (const key in node) if (key !== "loc") collect(node[key]); - } + }, + string: () => {}, }; // Both walks only record candidates: a function without one holds none. - if (candidates.size > 0) collect(fn.body); + 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; @@ -333,24 +332,22 @@ 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 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); + }, }; // Only candidates declared from a charAt or an optional read can be held - // (understoodMention), and count() only decides which are. - if (reads.size > 0 || charAtDeclared) count(fn.body); + // (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(); @@ -492,14 +489,12 @@ function unitLocal(local: IrLocal): IrLocal { * 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( - node: Record, + n: IrExpr | IrStmt, locals: ReadonlyMap, reads: ReadonlyMap, localTags: (operand: IrExpr) => OptionalCharRead | null, ): 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; + const kind = n.kind; if ( kind !== "varDecl" && kind !== "strEq" && @@ -510,7 +505,6 @@ function understoodMention( kind !== "unionIsTag" ) return null; - const n = node as unknown as IrExpr | IrStmt; if (n.kind === "varDecl") { if (!n.init) return null; const local = locals.get(n.localId); diff --git a/packages/compiler/src/ir/fields.generated.ts b/packages/compiler/src/ir/fields.generated.ts new file mode 100644 index 000000000..86c6a15e5 --- /dev/null +++ b/packages/compiler/src/ir/fields.generated.ts @@ -0,0 +1,2320 @@ +// 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; + } + } +} + +/** A field ir.ts declares that this file does not read. */ +type FieldsNotRead = Exclude; +/** The string or object values a field read as a scalar can hold. */ +type ValuesNotRead = Extract, string | object>; +/** Fails type checking unless `T` is never: run `pnpm generate:ir-fields`. */ +type Unread = T; + +/** Every walked object type's fields as ir.ts declared them when this file + * was generated. */ +export type IrFieldsCurrent = [ + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc">>, + Unread, "kind" | "arr" | "index" | "type" | "loc">>, + Unread, "kind" | "arr" | "index" | "type" | "loc">>, + Unread, "kind" | "elems" | "spreads" | "capacity" | "type" | "loc">>, + Unread["capacity"]>>, + Unread, "kind" | "length" | "type" | "loc">>, + Unread, "kind" | "arr" | "index" | "type" | "loc">>, + Unread, "kind" | "localId" | "value" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "adopt" | "loc">>, + Unread["adopt"]>>, + Unread, "kind" | "value" | "promiseTag" | "type" | "loc">>, + Unread["promiseTag"]>>, + Unread, "kind" | "op" | "left" | "right" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread["value"]>>, + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc" | "invalidNaN">>, + Unread["invalidNaN"]>>, + Unread, "kind" | "source" | "from" | "type" | "loc">>, + Unread["from"]>>, + Unread, "kind" | "callee" | "args" | "type" | "loc">>, + Unread, "kind" | "callee" | "receiver" | "args" | "type" | "loc">>, + Unread, "kind" | "value" | "className" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "value" | "test" | "className" | "negated" | "type" | "loc">>, + Unread["negated"]>>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "id" | "type" | "loc">>, + Unread, "kind" | "className" | "captures" | "type" | "loc">>, + Unread, "kind" | "fnName" | "captures" | "adapts" | "type" | "loc">>, + Unread["adapts"]>>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "elems" | "type" | "loc">>, + Unread["spreads"]>, readonly unknown[]>[number]>, object>, "arg" | "what">>, + Unread["spreads"]>, readonly unknown[]>[number]>, object>["arg"]>>, + Unread, "kind" | "callee" | "receiver" | "calleeName" | "calleeNameValue" | "args" | "spreads" | "type" | "loc">>, + Unread, "kind" | "value" | "preserveRefs" | "adopt" | "exact" | "inferredSite" | "origin" | "type" | "loc">>, + Unread["preserveRefs"]>>, + Unread["adopt"]>>, + Unread["exact"]>>, + Unread["origin"]>>, + Unread, "kind" | "value" | "spelling" | "firstProp" | "type" | "loc">>, + Unread, "kind" | "value" | "fnName" | "fnLength" | "liveRef" | "type" | "loc">>, + Unread["fnLength"]>>, + Unread["liveRef"]>>, + Unread, "kind" | "key" | "negated" | "value" | "type" | "loc">>, + Unread["negated"]>>, + Unread, "kind" | "recv" | "method" | "calleeName" | "calleeNameValue" | "args" | "type" | "loc">>, + Unread, "kind" | "value" | "count" | "notIterableMessage" | "type" | "loc">>, + Unread["count"]>>, + Unread, "kind" | "key" | "optional" | "value" | "type" | "loc">>, + Unread["optional"]>>, + Unread["fields"]>, readonly unknown[]>[number]>, object>, "key" | "value">>, + Unread, "kind" | "fields" | "type" | "loc">>, + Unread, "kind" | "left" | "right" | "negated" | "type" | "loc">>, + Unread["negated"]>>, + Unread, "kind" | "test" | "bytesElem" | "negated" | "value" | "type" | "loc">>, + Unread["negated"]>>, + Unread, "kind" | "import" | "args" | "type" | "loc">>, + Unread, "kind" | "unionId" | "type" | "loc">>, + Unread, "kind" | "obj" | "className" | "field" | "type" | "loc">>, + Unread, "kind" | "op" | "prefix" | "obj" | "className" | "field" | "fieldDyn" | "type" | "loc">>, + Unread["prefix"]>>, + Unread["fieldDyn"]>>, + Unread, "kind" | "mode" | "gen" | "arg" | "type" | "loc">>, + Unread, "kind" | "op" | "prefix" | "localId" | "type" | "loc">>, + Unread["prefix"]>>, + Unread, "kind" | "value" | "className" | "type" | "loc">>, + Unread, "kind" | "value" | "classValue" | "type" | "loc">>, + Unread, "kind" | "name" | "args" | "type" | "loc">>, + Unread, "kind" | "value" | "indent" | "circular" | "type" | "loc">>, + Unread["circular"]>>, + Unread["assertOptions"]>, object>, "diff" | "skipPrototype">>, + Unread["assertOptions"]>, object>["skipPrototype"]>>, + Unread, "kind" | "fn" | "args" | "type" | "loc" | "prototypeIdentityOnly" | "realTimer" | "assertOptions">>, + Unread["prototypeIdentityOnly"]>>, + Unread["realTimer"]>>, + Unread, "kind" | "op" | "left" | "right" | "type" | "loc">>, + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc">>, + Unread["seed"]>, readonly unknown[]>[number]>, object>, "key" | "value">>, + Unread, "kind" | "seed" | "type" | "loc">>, + Unread, "kind" | "moduleId" | "type" | "loc">>, + Unread, "kind" | "className" | "args" | "type" | "loc">>, + Unread, "kind" | "executor" | "type" | "loc">>, + Unread, "kind" | "callee" | "args" | "type" | "loc">>, + Unread, "kind" | "left" | "right" | "type" | "loc">>, + Unread, "kind" | "value" | "spelling" | "type" | "loc">>, + Unread["value"]>>, + Unread, "kind" | "id" | "receiver" | "body" | "type" | "loc">>, + Unread, "kind" | "left" | "right" | "retag" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "type" | "loc">>, + Unread["overrides"]>, readonly unknown[]>[number]>, object>, "name" | "value">>, + Unread, "kind" | "source" | "overrides" | "type" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "field" | "type" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "field" | "type" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "key" | "overflowOnly" | "missNaN" | "type" | "loc">>, + Unread["overflowOnly"]>>, + Unread["missNaN"]>>, + Unread["fields"]>, readonly unknown[]>[number]>, object>, "name" | "value" | "overflow">>, + Unread["fields"]>, readonly unknown[]>[number]>, object>["overflow"]>>, + Unread, "kind" | "fields" | "nullProto" | "type" | "loc">>, + Unread["nullProto"]>>, + Unread, "kind" | "obj" | "shapeId" | "type" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "key" | "type" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "type" | "loc">>, + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc">>, + Unread, "kind" | "pattern" | "flags" | "type" | "loc">>, + Unread, "kind" | "type" | "loc">>, + Unread, "kind" | "stmts" | "result" | "generatorDelegate" | "diverges" | "type" | "loc">>, + Unread["generatorDelegate"]>>, + Unread["diverges"]>>, + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc">>, + Unread, "kind" | "seed" | "type" | "loc">>, + Unread, "kind" | "op" | "left" | "right" | "type" | "loc">>, + Unread, "kind" | "left" | "right" | "type" | "loc">>, + Unread, "kind" | "negated" | "left" | "right" | "type" | "loc">>, + Unread["negated"]>>, + Unread, "kind" | "method" | "receiver" | "args" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "key" | "cooked" | "type" | "loc">>, + Unread, "kind" | "cond" | "then" | "else_" | "type" | "loc">>, + Unread, "kind" | "operand" | "type" | "loc">>, + Unread, "kind" | "operand" | "type" | "loc">>, + Unread, "kind" | "op" | "operand" | "type" | "loc">>, + Unread, "kind" | "unionId" | "field" | "value" | "type" | "loc">>, + Unread, "kind" | "unionId" | "negated" | "sameValue" | "left" | "right" | "type" | "loc">>, + Unread["negated"]>>, + Unread["sameValue"]>>, + Unread, "kind" | "unionId" | "tag" | "union" | "func" | "negated" | "type" | "loc">>, + Unread["tag"]>>, + Unread["negated"]>>, + Unread, "kind" | "unionId" | "tag" | "negated" | "value" | "type" | "loc">>, + Unread["tag"]>>, + Unread["negated"]>>, + Unread, "kind" | "unionId" | "key" | "value" | "type" | "loc">>, + Unread, "kind" | "unionId" | "tag" | "value" | "type" | "loc">>, + Unread["tag"]>>, + Unread, "kind" | "fromId" | "unionId" | "tags" | "value" | "type" | "loc">>, + Unread, "kind" | "unionId" | "tag" | "value" | "type" | "loc">>, + Unread["tag"]>>, + Unread, "kind" | "unit" | "type" | "loc">>, + Unread, "kind" | "value" | "type" | "loc">>, + Unread, "kind" | "localId" | "type" | "loc">>, + Unread, "kind" | "className" | "method" | "args" | "type" | "loc" | "targets">>, + Unread["captureCompletion"]>, object>, "returnType">>, + Unread, "kind" | "value" | "awaited" | "delegated" | "captureCompletion" | "type" | "loc">>, + Unread["awaited"]>>, + Unread["delegated"]>>, + Unread, "kind" | "arr" | "index" | "loc">>, + Unread, "kind" | "arr" | "index" | "value" | "loc">>, + Unread, "kind" | "arr" | "length" | "loc" | "pop" | "reserve">>, + Unread["pop"]>>, + Unread["reserve"]>>, + Unread, "kind" | "arr" | "index" | "loc">>, + Unread, "kind" | "localId" | "value" | "initializes" | "loc">>, + Unread["initializes"]>>, + Unread, "kind" | "body" | "labels" | "loc">>, + Unread, "kind" | "label" | "loc">>, + Unread, "kind" | "arr" | "index" | "value" | "loc">>, + Unread, "kind" | "label" | "loc">>, + Unread, "kind" | "body" | "cond" | "labels" | "loc">>, + Unread, "kind" | "expr" | "loc">>, + Unread, "kind" | "obj" | "className" | "field" | "value" | "sloppy" | "define" | "attributesChecked" | "loc">>, + Unread["sloppy"]>>, + Unread["define"]>>, + Unread["attributesChecked"]>>, + Unread, "kind" | "init" | "cond" | "update" | "body" | "labels" | "loc">>, + Unread, "kind" | "localId" | "iterable" | "body" | "labels" | "loc">>, + Unread, "kind" | "cond" | "then" | "else_" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "key" | "loc">>, + Unread, "kind" | "obj" | "shapeId" | "key" | "value" | "overflowOnly" | "loc">>, + Unread["overflowOnly"]>>, + Unread, "kind" | "obj" | "shapeId" | "field" | "value" | "loc">>, + Unread, "kind" | "localId" | "loc">>, + Unread, "kind" | "value" | "awaited" | "loc">>, + Unread["awaited"]>>, + Unread, "kind" | "code" | "message" | "loc">>, + Unread["cases"]>, readonly unknown[]>[number]>, object>, "test" | "body">>, + Unread, "kind" | "disc" | "cases" | "labels" | "loc">>, + Unread, "kind" | "value" | "loc">>, + Unread, "kind" | "tryBody" | "catchBody" | "catchLocalId" | "finallyBody" | "suppressFinallyErrors" | "dropFinallyErrors" | "loc">>, + Unread["suppressFinallyErrors"]>>, + Unread["dropFinallyErrors"]>>, + Unread, "kind" | "localId" | "init" | "loc">>, + Unread, "kind" | "cond" | "body" | "labels" | "loc">>, + Unread, "kind" | "elem" | "optionalElements">>, + Unread["optionalElements"]>>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "elem">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "className">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "params" | "ret" | "rest" | "restAbi" | "argumentsAll">>, + Unread["rest"]>>, + Unread["argumentsAll"]>>, + Unread, "kind" | "async" | "yieldT" | "retT" | "nextT">>, + Unread["async"]>>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "key" | "value">>, + Unread, "kind" | "moduleId">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "className">>, + Unread, "kind">>, + Unread, "kind" | "inner">>, + Unread, "kind" | "shapeId">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "elem">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind">>, + Unread, "kind" | "unionId">>, + Unread, "kind">>, + Unread, "kind">>, +]; 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 4101ae2fd..d54fc3967 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,6 +158,8 @@ 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", @@ -223,6 +226,9 @@ const MODULE_FIELDS: Record>, Role> = { "threadSlotKeys[]": "other", "foreignElementKeys[]": "other", "freezableArrays[]": "other", + "sloppyDynWrites[]": "other", + "inferredFactFiles[].file": "other", + "inferredFactFiles[].pkg": "other", "declOrigins[]": "other", }; @@ -285,6 +291,14 @@ 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 }], diff --git a/packages/compiler/src/ir/function-references.ts b/packages/compiler/src/ir/function-references.ts index 6696a3c11..61e6e2797 100644 --- a/packages/compiler/src/ir/function-references.ts +++ b/packages/compiler/src/ir/function-references.ts @@ -24,6 +24,8 @@ 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-slots.ts b/packages/compiler/src/ir/int32-slots.ts index fbd51dbc5..be5f29768 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -520,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", @@ -1480,6 +1484,39 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): continue; } const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e)); + // The range of an exact integer that may also be NaN: an element read + // of a candidate through `getNumber` (`a[k]!` in arithmetic, NaN for a + // missing element), and `+`, `-` and `*` over such values and integers. + // A checked store widens the array at a NaN like at any value that is + // not an int32. + const integerOrNaN = (e: IrExpr): IntegerRange | null => { + const range = ranges.get(e); + if (range) return range; + if (e.kind === "numLit") + return Number.isInteger(e.value) ? { min: e.value, max: e.value } : null; + if (e.kind === "arrIntrinsic" && e.method === "getNumber" && e.receiver.kind === "varRef") + return slots.hasInt32ElementSlot(int32ArraySlot(int32ArrayKey(fn.name, e.receiver.localId))) + ? INT32_RANGE + : null; + if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-" && e.op !== "*")) return null; + const left = integerOrNaN(e.left); + const right = left && integerOrNaN(e.right); + if (!left || !right) return null; + const bounds = + e.op === "+" + ? [left.min + right.min, left.max + right.max] + : e.op === "-" + ? [left.min - right.max, left.max - right.min] + : [ + left.min * right.min, + left.min * right.max, + left.max * right.min, + left.max * right.max, + ]; + const min = Math.min(...bounds); + const max = Math.max(...bounds); + return Math.abs(min) < 2 ** 53 && Math.abs(max) < 2 ** 53 ? { min, max } : null; + }; // Every element stored into an int32 array candidate must be an int32 // under the facts that assume the candidates' own elements are; a // failing store drops its array's element facts, and the function is @@ -1509,6 +1546,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const [id, values] of int32ArrayStores(fn, arrays)) { const key = int32ArrayKey(fn.name, id); if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue; + // Checked stores serve a lone local: its allocations are the only + // arrays it holds. Its stores are judged under the element facts + // the ranges were computed with. + const checkable = values.every((value) => int32(value) || integerOrNaN(value) !== null); // The array's group drops together: every function reading a // member is analyzed again without its facts. const group = arrayGroups.group.get(key) ?? [key]; @@ -1519,17 +1560,11 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): 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 ( group.length === 1 && !key.startsWith("\0") && !params.has(id) && - values.every(checkable) && + checkable && checkableInt32Allocations(fn, id) ) slots.checkedInt32Arrays.add(key); diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts index 51c0a929b..23e2df90a 100644 --- a/packages/compiler/src/ir/integer-ranges.ts +++ b/packages/compiler/src/ir/integer-ranges.ts @@ -23,6 +23,11 @@ export interface IntegerRangeFacts { ranges: IntegerRanges; loopRanges: ReadonlyMap>; } +/** Ranges assumed for locals on entry to while loops (loop-entry-ranges.ts): + * the facts inside such a loop hold in a copy of it that runs only after a + * test proves every assumed local is an integer in its range. After the + * loop, its assumed locals and its writes carry no facts. */ +export type LoopEntryAssumptions = ReadonlyMap>; const SIGNED: IntegerRange = { min: -2147483648, max: 2147483647 }; const UNSIGNED: IntegerRange = { min: 0, max: 4294967295 }; /** Int32 and uint32 at once: the bound under which `%` (modeled only for @@ -309,6 +314,7 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = { export function analyzeIntegerRangeFacts( fn: IrFunction, slots: IntegerSlotFacts = {}, + entries?: LoopEntryAssumptions, ): IntegerRangeFacts { const ranges = new Map(); const loopRanges = new Map>(); @@ -1074,6 +1080,8 @@ export function analyzeIntegerRangeFacts( } case "while": case "doWhile": { + const assumed = entries?.get(s); + if (assumed) for (const [id, range] of assumed) if (!facts.has(id)) set(facts, id, range); // Counters the condition bounds keep their range at every read // inside and after the loop. const guarded = @@ -1102,6 +1110,10 @@ export function analyzeIntegerRangeFacts( } else body(s.body, new Map(loop)); havocWith(facts, [s], facts, guarded); for (const [id, range] of guarded) set(facts, id, range); + if (assumed) { + invalidate(facts, [s]); + for (const id of assumed.keys()) facts.delete(id); + } break; } case "forOf": { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index f82e71d4a..5a6967168 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1063,6 +1063,11 @@ 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, @@ -1084,6 +1089,11 @@ export interface IrModule { * (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[]; + /** Source ranges ("file\0start\0end") of untyped property writes in + * sloppy-mode code (frontend frozen-records.ts sloppyDynamicWrites): + * their dyn.keySet/keySetComputed calls ignore an assignment that strict + * mode refuses with a TypeError. */ + sloppyDynWrites?: 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 @@ -1537,6 +1547,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 @@ -4055,6 +4076,10 @@ export type 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; diff --git a/packages/compiler/src/ir/loop-entry-ranges.test.ts b/packages/compiler/src/ir/loop-entry-ranges.test.ts new file mode 100644 index 000000000..ad1b78ef6 --- /dev/null +++ b/packages/compiler/src/ir/loop-entry-ranges.test.ts @@ -0,0 +1,143 @@ +import { expect, test } from "vitest"; +import { analyzeIntegerRangeFacts } from "./integer-ranges.js"; +import { findLoopEntryVersions, LOOP_ENTRY_RANGE } from "./loop-entry-ranges.js"; +import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js"; + +const loc = { file: "test.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 condition = (op: "<" | "===", left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "bin", + op, + left, + right, + type: BOOL, +}); +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 observe = (value: IrExpr): IrStmt => ({ loc, kind: "exprStmt", expr: value }); +const fn = (body: IrStmt[], locals: string[]): IrFunction => ({ + loc, + name: "f", + params: [], + returnType: F64, + locals: locals.map((id) => ({ id, name: id, type: F64, mutable: true })), + body, +}); + +/** base = unknown; breaks = unknown; count = 0; i = 0; + * while (i < 300) { ...extra; i++; count++; last = (count + base - breaks) % 8; + * if (last === 0) breaks++; } + * observe(base) */ +function lineBreaks(extra: IrStmt[] = [], locals: string[] = []) { + const remainder = bin("%", bin("-", bin("+", ref("count"), ref("base")), ref("breaks")), num(8)); + const after = ref("base"); + const loop: IrStmt = { + loc, + kind: "while", + cond: condition("<", ref("i"), num(300)), + body: [ + ...extra, + bump("i"), + bump("count"), + set("last", remainder), + { + loc, + kind: "if", + cond: condition("===", ref("last"), num(0)), + then: [bump("breaks")], + else_: null, + }, + ], + }; + const f = fn( + [ + set("base", ref("unknown")), + set("breaks", ref("unknown")), + set("count", num(0)), + set("i", num(0)), + set("last", num(1)), + loop, + observe(after), + ], + ["base", "breaks", "count", "i", "last", ...locals], + ); + return { f, loop, remainder, after }; +} + +test("versions a while loop whose remainder reads locals without entry facts", () => { + const { f, loop, remainder } = lineBreaks(); + const facts = analyzeIntegerRangeFacts(f); + expect(facts.ranges.get(remainder)).toBeFalsy(); + const version = findLoopEntryVersions(f, facts).get(loop); + expect(version).toBeDefined(); + expect([...version!.entry]).toEqual([ + ["base", LOOP_ENTRY_RANGE], + ["breaks", LOOP_ENTRY_RANGE], + ]); + // The dividend is an exact integer; `%` itself is modeled only for + // non-negative dividends. + const dividend = (remainder as Extract).left; + expect(version!.ranges.get(dividend)).toBeDefined(); + // The loop's counters, `breaks` among them, are bounded in the copy. + expect(version!.loopRanges?.get("breaks")).toEqual({ + min: LOOP_ENTRY_RANGE.min, + max: LOOP_ENTRY_RANGE.max + 300, + }); +}); + +test("the entry test covers only the locals the remainder depends on", () => { + // acc = acc + scale reads two more locals without facts. + const { f, loop } = lineBreaks( + [set("acc", bin("+", ref("acc"), ref("scale")))], + ["acc", "scale"], + ); + f.body.unshift(set("acc", ref("unknown")), set("scale", ref("unknown"))); + const version = findLoopEntryVersions(f, analyzeIntegerRangeFacts(f)).get(loop); + expect([...version!.entry.keys()]).toEqual(["base", "breaks"]); +}); + +test("assumed facts do not leave the loop", () => { + const { f, loop, after } = lineBreaks(); + const assumed = new Map([[loop, new Map([["base", LOOP_ENTRY_RANGE]])]]); + expect(analyzeIntegerRangeFacts(f, {}, assumed).ranges.get(after)).toBeFalsy(); +}); + +test("loops without a remainder the assumption proves keep one copy", () => { + // A local the loop makes fractional, a product without a remainder, and a + // remainder the function already proves. + const fractional = lineBreaks([set("base", bin("+", ref("base"), num(0.5)))]); + expect(findLoopEntryVersions(fractional.f, analyzeIntegerRangeFacts(fractional.f)).size).toBe(0); + const product: IrStmt = { + loc, + kind: "while", + cond: condition("<", ref("i"), num(300)), + body: [set("acc", bin("*", ref("acc"), ref("scale"))), bump("i")], + }; + const products = fn( + [set("acc", ref("unknown")), set("scale", ref("unknown")), set("i", num(0)), product], + ["acc", "scale", "i"], + ); + expect(findLoopEntryVersions(products, analyzeIntegerRangeFacts(products)).size).toBe(0); + const proven: IrStmt = { + loc, + kind: "while", + cond: condition("<", ref("i"), num(300)), + body: [set("acc", bin("%", ref("i"), num(7))), bump("i")], + }; + const known = fn([set("acc", ref("unknown")), set("i", num(0)), proven], ["acc", "i"]); + expect(findLoopEntryVersions(known, analyzeIntegerRangeFacts(known)).size).toBe(0); +}); diff --git a/packages/compiler/src/ir/loop-entry-ranges.ts b/packages/compiler/src/ir/loop-entry-ranges.ts new file mode 100644 index 000000000..a5b7628be --- /dev/null +++ b/packages/compiler/src/ir/loop-entry-ranges.ts @@ -0,0 +1,218 @@ +import type { IrExpr, IrFunction, IrStmt } from "./ir.js"; +import { everyExpr, everyStmt, everyStmtList } from "./traverse.js"; +import { + analyzeIntegerRangeFacts, + type IntegerRange, + type IntegerRangeFacts, + type IntegerSlotFacts, + type LoopEntryAssumptions, +} from "./integer-ranges.js"; + +type WhileLoop = Extract; + +/** The range a versioned loop requires of each assumed local on entry. A + * loop's counters stay below a typed array or string length (at most 2^48), + * so sums of a few such values and assumed locals stay exact integers. */ +export const LOOP_ENTRY_RANGE: IntegerRange = { min: -(2 ** 48), max: 2 ** 48 }; + +/** A copy of a while loop that runs when every local in `entry` holds an + * integer in its range on entry (never -0). `ranges` are the facts that + * hold for the loop's expressions in that copy and that the function's own + * analysis does not prove; `loopRanges` are the loop's bounded bindings + * there. */ +export interface LoopEntryVersion { + entry: ReadonlyMap; + ranges: ReadonlyMap; + loopRanges: ReadonlyMap | undefined; +} + +/** Versions of innermost while loops whose integer work depends on locals + * that enter the loop without range facts: copies of module globals, + * parameters, fields or call results (`const base = offset;` before a loop + * that computes `(count + base) % 60`). + * + * The function is analyzed again with each number local the loop reads + * without a fact assumed in LOOP_ENTRY_RANGE at the loop's entry. A loop is + * versioned when that analysis proves both operands of a remainder that the + * function's own analysis leaves unproven. Only remainders qualify: the + * operands of a product or a sum of such locals are as often fractional, + * and a loop whose test fails runs the ordinary copy in a larger function. + * The entry test covers only the assumed locals that such an operation reads, directly, + * through the loop's writes of other locals, or through the loop condition; + * when that drops any, the function is analyzed once more with the smaller + * sets, so the facts never rest on a local the test does not check. No + * fact leaves an assumed loop, so the loops' assumptions are independent. */ +export function findLoopEntryVersions( + fn: IrFunction, + facts: IntegerRangeFacts, + slots: IntegerSlotFacts = {}, +): Map { + const result = new Map(); + if (fn.async || fn.generator) return result; + const captures = new Set( + [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), + ); + const eligible = new Set( + fn.locals + .filter((l) => l.type.kind === "f64" && !l.boxed && !l.tdz && !captures.has(l.id)) + .map((l) => l.id), + ); + const known = (e: IrExpr): boolean => !!facts.ranges.get(e); + let candidates = new Map>(); + everyStmtList(fn.body, { + expr: () => true, + stmt: (s) => { + if (s.kind !== "while" || containsLoop(s.body)) return true; + const assumed = entryCandidates(s, eligible, known); + if (assumed.size > 0) candidates.set(s, assumed); + return true; + }, + }); + if (candidates.size === 0) return result; + let versioned = analyzeIntegerRangeFacts(fn, slots, assumptionsOf(candidates)); + const pruned = new Map>(); + let dropped = false; + for (const [loop, entry] of candidates) { + const roots = gainedOperands(loop, known, versioned); + if (roots.length === 0) continue; + const relevant = relevantLocals(loop, roots); + const kept = new Map(); + for (const [id, range] of entry) if (relevant.has(id)) kept.set(id, range); + if (kept.size < entry.size) dropped = true; + if (kept.size > 0) pruned.set(loop, kept); + } + if (dropped) { + candidates = pruned; + if (candidates.size === 0) return result; + versioned = analyzeIntegerRangeFacts(fn, slots, assumptionsOf(candidates)); + } + for (const [loop, entry] of candidates) { + if (gainedOperands(loop, known, versioned).length === 0) continue; + const ranges = new Map(); + const visit = (e: IrExpr): boolean => { + const range = versioned.ranges.get(e); + const prior = facts.ranges.get(e); + if (range && !prior) ranges.set(e, range); + else if (range && prior && (range.min > prior.min || range.max < prior.max)) + ranges.set(e, { min: Math.max(range.min, prior.min), max: Math.min(range.max, prior.max) }); + return true; + }; + everyStmt(loop, { expr: visit, stmt: () => true }); + result.set(loop, { entry, ranges, loopRanges: versioned.loopRanges.get(loop) }); + } + return result; +} + +function assumptionsOf( + candidates: ReadonlyMap>, +): LoopEntryAssumptions { + const result = new Map>(); + for (const [loop, entry] of candidates) result.set(loop, entry); + return result; +} + +/** Number locals the loop reads without a range fact and does not declare, + * when the loop has a remainder without one. */ +function entryCandidates( + loop: WhileLoop, + eligible: ReadonlySet, + known: (e: IrExpr) => boolean, +): Map { + const read = new Set(); + const declared = new Set(); + let integerWork = false; + everyStmt(loop, { + expr: (e) => { + if (e.kind === "varRef" && eligible.has(e.localId) && !known(e)) read.add(e.localId); + else if (e.kind === "bin" && e.op === "%" && !known(e)) integerWork = true; + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl") declared.add(s.localId); + return true; + }, + }); + const result = new Map(); + if (!integerWork) return result; + for (const id of read) if (!declared.has(id)) result.set(id, LOOP_ENTRY_RANGE); + return result; +} + +/** Operands of the loop's remainders that the versioned analysis proves + * where the function's own analysis does not prove both. */ +function gainedOperands( + loop: WhileLoop, + known: (e: IrExpr) => boolean, + versioned: IntegerRangeFacts, +): IrExpr[] { + const operands: IrExpr[] = []; + everyStmt(loop, { + expr: (e) => { + if ( + e.kind === "bin" && + e.op === "%" && + !(known(e.left) && known(e.right)) && + !!versioned.ranges.get(e.left) && + !!versioned.ranges.get(e.right) + ) + operands.push(e.left, e.right); + return true; + }, + stmt: () => true, + }); + return operands; +} + +/** The locals `roots` read, closed over the loop's writes (a local written + * from others reads them) and over its condition (a local the condition + * reads makes every local of the condition relevant). */ +function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set { + const relevant = new Set(); + const readsOf = (e: IrExpr): string[] => { + const ids: string[] = []; + everyExpr(e, { + expr: (x) => { + if (x.kind === "varRef") ids.push(x.localId); + return true; + }, + stmt: () => true, + }); + return ids; + }; + const written = new Map(); + everyStmt(loop, { + expr: (e) => { + if (e.kind === "assignExpr") + written.set(e.localId, [...(written.get(e.localId) ?? []), e.value]); + return true; + }, + stmt: (s) => { + const value = s.kind === "assign" ? s.value : s.kind === "varDecl" ? s.init : undefined; + if (value && (s.kind === "assign" || s.kind === "varDecl")) + written.set(s.localId, [...(written.get(s.localId) ?? []), value]); + return true; + }, + }); + const condition = readsOf(loop.cond); + const pending: string[] = []; + const add = (id: string): void => { + if (relevant.has(id)) return; + relevant.add(id); + pending.push(id); + }; + for (const root of roots) for (const id of readsOf(root)) add(id); + while (pending.length > 0) { + const id = pending.pop()!; + for (const value of written.get(id) ?? []) for (const other of readsOf(value)) add(other); + if (condition.includes(id)) for (const other of condition) add(other); + } + return relevant; +} + +function containsLoop(body: IrStmt[]): boolean { + return !everyStmtList(body, { + expr: () => true, + stmt: (s) => + s.kind !== "while" && s.kind !== "doWhile" && s.kind !== "for" && s.kind !== "forOf", + }); +} 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 5156d4bff..ca2958585 100644 --- a/packages/compiler/src/ir/parameter-versions.ts +++ b/packages/compiler/src/ir/parameter-versions.ts @@ -122,7 +122,7 @@ function numericLoop(loop: IrStmt, functions: ReadonlyMap): const target = functions.get(e.callee); allowed = target !== undefined && simple(target); } else if (e.kind === "libCall") allowed = e.fn.startsWith("math."); - else if (e.kind === "arrIntrinsic") allowed = e.method === "length"; + else if (e.kind === "arrIntrinsic") allowed = e.method === "length" || e.method === "getNumber"; return allowed && everyExprChild(e, expr, stmt); }; const stmt = (s: IrStmt): boolean => diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 056449a3c..0fb4ce815 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,7 +9,7 @@ import { 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"; @@ -967,7 +967,10 @@ 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 }), @@ -1366,7 +1369,7 @@ function scanRuntimeFeatures( for (const t of nodes.types) type(t); } else if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type }); for (const { name, scan } of scans) if (scan.result()) features[name] = true; - // Inline-cache sites of dynamic-tier functions use the tier's shapes + // Inline-cache sites of dynamic-tier functions use the tier's caches // (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c). if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) { const sites = new JsIcSites(mod); @@ -1380,6 +1383,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/traverse.ts b/packages/compiler/src/ir/traverse.ts index 80425a056..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; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 51a426d23..a1ca8f9c7 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -3026,6 +3026,18 @@ function validateFunction( } 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; @@ -3133,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); } @@ -4223,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; diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts index 0250e9618..551a151a1 100644 --- a/packages/compiler/src/native/cache.ts +++ b/packages/compiler/src/native/cache.ts @@ -170,6 +170,7 @@ export class NativeCache { "kept", "linker-job", "runtime-pack", + "helper", "binary", "dsym", "coverage", diff --git a/packages/compiler/src/native/compiler.test.ts b/packages/compiler/src/native/compiler.test.ts index a085356a0..c9636ea76 100644 --- a/packages/compiler/src/native/compiler.test.ts +++ b/packages/compiler/src/native/compiler.test.ts @@ -107,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"); @@ -250,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 = { @@ -341,7 +341,7 @@ test.skipIf(process.platform === "win32").each([false, true])( }); vi.mocked(emitNativeObject).mockImplementation((options) => { writeFileSync(options.outputPath, "object"); - return [options.outputPath]; + return { outputs: [options.outputPath], profileMatch: null }; }); vi.mocked(openNativeExecutableCache).mockReturnValue(null); const links: string[][] = []; @@ -378,3 +378,84 @@ test.skipIf(process.platform === "win32").each([false, true])( 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 f2734d2ee..0dec85cbd 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -28,6 +28,7 @@ import { requireNativeArtifact, runNativeTool, verifyNativeHelper, + type NativeHelperVerification, } from "../backend/native-tools.js"; import { type NativeRuntimeSelection, @@ -39,6 +40,7 @@ import { import { RuntimePackError } from "../backend/runtime-pack-core.js"; import { NativeCodegenError, + nativeCodegenParts, nativePartitionPaths, nativeProgramPartitions, } from "../backend/native-codegen-core.js"; @@ -63,21 +65,41 @@ import { contentDigest, fileDigest, partsDigest, type NativeCache } from "./cach 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 { inputsWithoutLld, lldLinkerArgs } from "../backend/lld.js"; import { openNativeExecutableCache } from "./executable-cache.js"; import { LinkerJobs } from "./linker-job.js"; import { nativeFileIdentity } from "./file-identity.js"; +import { + staticExecutableRefusal, + staticLinkFailureNote, + staticLinkMapArgs, + staticLinkRefusal, +} from "../backend/targets.js"; /** The first partition keeps the whole-object key used before partitioning. */ 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, @@ -177,6 +199,44 @@ export class NativeCompiler { }; } + /** The helpers whose `version` answer this cache recorded, by file identity: + * the probe runs again only when the helper file changes. */ + private helperVerification(): { verified?: NativeHelperVerification } { + const cache = this.cache; + const toolchain = this.toolchain; + if (cache === null) return {}; + let identity: string; + let key: string; + try { + identity = nativeFileIdentity(toolchain.helperExecutable); + key = contentDigest( + JSON.stringify({ + schema: 1, + helper: identity, + compiler: nativeFileIdentity(process.execPath), + version: toolchain.compilerVersion, + target: toolchain.target.name, + package: toolchain.helper.packageName, + }), + ); + } catch { + return {}; + } + return { + verified: { + known: () => cache.read("helper", key) !== null, + remember: () => { + try { + if (nativeFileIdentity(toolchain.helperExecutable) === identity) + cache.write("helper", key, "verified\n"); + } catch { + /* A failed write only means probing again next time. */ + } + }, + }, + }; + } + private emitObject( input: string, output: string, @@ -186,7 +246,8 @@ export class NativeCompiler { partitions = 1, profile?: NativeProfileOptions, llvmDigest: () => string = () => fileDigest(input), - ): string[] { + splitCodegen = false, + ): NativeObjects { const toolchain = this.toolchain; const outputs = nativePartitionPaths(output, partitions); const helperOptions = { @@ -195,6 +256,7 @@ export class NativeCompiler { compilerVersion: toolchain.compilerVersion, target: toolchain.target, helper: toolchain.helper, + ...this.helperVerification(), }; let key: string | null = null; let identity: string | null = null; @@ -213,6 +275,7 @@ export class NativeCompiler { identity: readFileSync(join(toolchain.helperPackageRoot, "package.json"), "utf8"), version: toolchain.compilerVersion, ...(partitions > 1 ? { partitions } : {}), + ...(splitCodegen ? { split: "codegen" } : {}), ...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }), }), ); @@ -223,7 +286,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; } @@ -232,35 +308,47 @@ export class NativeCompiler { } } const partitionCache = - this.cache === null || identity === null || partitions < 2 + this.cache === null || identity === null || partitions < 2 || splitCodegen ? null : this.cache.partitionDirectory(identity); - emitNativeObject({ - executable: toolchain.helperExecutable, - packageRoot: toolchain.helperPackageRoot, - compilerVersion: toolchain.compilerVersion, - target: toolchain.target, - helper: toolchain.helper, + const { profileMatch } = emitNativeObject({ + ...helperOptions, inputPath: input, outputPath: output, sourcePath: source, optimization, outputKind, partitions, + ...(splitCodegen ? { splitCodegen } : {}), ...(partitionCache === null ? {} : { partitionCache }), ...(profile === undefined ? {} : { profile }), }); 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( @@ -272,7 +360,7 @@ export class NativeCompiler { library: boolean, profile?: NativeProfileOptions, llvmDigest?: () => string, - ): string[] { + ): NativeObjects { // Library archives merge dev shards relocatably; executables link the // helper's program partitions directly. const llvm = @@ -281,7 +369,11 @@ export class NativeCompiler { : null; const split = llvm === null ? null : splitLlvmLibraryProgram(llvm); if (split === null) { - const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size); + const size = statSync(input).size; + // An optimized program that stays one partition is optimized whole and + // generates code for its parts concurrently. + const codegenParts = library || optimization === "dev" ? 1 : nativeCodegenParts(size); + const partitions = library ? 1 : Math.max(nativeProgramPartitions(size), codegenParts); return this.emitObject( input, output, @@ -291,6 +383,7 @@ export class NativeCompiler { partitions, profile, llvmDigest, + codegenParts > 1, ); } const objects: string[] = []; @@ -303,7 +396,7 @@ export class NativeCompiler { } const merged = localizeNativeLibrary(this.toolchain, stage, objects, [], split.publicSymbols); renameSync(merged, output); - return [output]; + return { paths: [output] }; } compile(entry: string, options: CompileRequestOptions): CompileRequestResult { @@ -353,6 +446,13 @@ export class NativeCompiler { platform: target.platform, }); if (profileRefusal !== null) throw new NativeCodegenError("SC3002", profileRefusal); + const staticRefusal = staticLinkRefusal({ + staticLink: options.staticLink, + outputKind, + sanitize: options.sanitize ?? false, + target, + }); + if (staticRefusal !== null) throw new NativeCodegenError("SC3002", staticRefusal); const profile = nativeProfile(options); let ffi: FfiProfile | null = null; if (options.ffiProfilePath !== undefined) { @@ -427,6 +527,9 @@ export class NativeCompiler { 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(); @@ -440,8 +543,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, @@ -451,7 +554,9 @@ export class NativeCompiler { profile, digestLlvm, ); - else + programObjects = objects.paths; + profileMatch = objects.profileMatch; + } else this.emitObject( llvmInput, object, @@ -515,6 +620,7 @@ export class NativeCompiler { runtimeSystemLibraries: runtime.systemLibraries, optimization, strip: options.strip ?? false, + staticLink: options.staticLink ?? false, ...(options.windowsSubsystem === undefined ? {} : { windowsSubsystem: options.windowsSubsystem }), @@ -522,18 +628,25 @@ export class NativeCompiler { }); // Linux links with lld when the driver finds it. const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker); - const linkArgs = [ + const linkMap = join(stage, "link.map"); + const mapArgs = staticLinkMapArgs(target, options.staticLink === true, linkMap); + const linkArgsWith = (withLld: boolean): string[] => [ ...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs), - ...lld, + ...(withLld ? lld : []), ...plan.driverFlags, - ...plan.inputs, + ...mapArgs, + ...(withLld ? plan.inputs : inputsWithoutLld(target.platform, plan.inputs)), ...systemLibraryLinkerArgs(target, plan.systemLibraries), "-o", stagedOutput, ]; + const linkArgs = linkArgsWith(lld.length !== 0); // 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 job = jobs?.link( + [...plan.inputs, stagedOutput, ...(mapArgs.length === 0 ? [] : [linkMap])], + stage, + ); const cacheable = executableCache?.trace(linkArgs, stage, job) ?? false; timing("link-inputs"); // A successful dependency trace already produced this executable. @@ -541,19 +654,26 @@ export class NativeCompiler { if (!cacheable && (job?.run(linkArgs, true) ?? null) === null) { const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker; try { - runNativeTool(driver, linkArgs); + 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, linkArgsWith(false)); + } } 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)), - ); + const note = staticLinkFailureNote(target, options.staticLink === true); + if (note === "") throw error; + throw new Error(`${error instanceof Error ? error.message : String(error)}${note}`); } } timing("native-link"); requireNativeArtifact(stagedOutput); + if (mapArgs.length !== 0) { + const refusal = staticExecutableRefusal(target, true, readFileSync(linkMap, "utf8")); + if (refusal !== null) throw new NativeCodegenError("SC3002", refusal); + } if (target.platform === "darwin" && optimization === "dev" && !options.strip) { runNativeTool(toolchain.dsymutil, [stagedOutput, "-o", stagedOutput + ".dSYM"]); } @@ -578,7 +698,13 @@ export class NativeCompiler { 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", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index dc6cdafa3..339a50faa 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -370,6 +370,8 @@ export function prepareNativeExecutable( const exclusions = { outputPaths, outputDirectories: [...directories] }; const irInput = process.env[BOOTSTRAP_IR_INPUT] ?? ""; let key: string | null = null; + // The entry this build replaces: its probes give the digests of unchanged files. + let previous: FrontendInputSnapshot | undefined; if (cache !== null && irInput === "" && provenanceSources() === null) { try { key = contentDigest( @@ -389,6 +391,7 @@ export function prepareNativeExecutable( timing("frontend-cache-hit"); return cached.input; } + if (cached !== null) previous = cached.stale.probes; const equivalent = cached === null || outputKind === "ir" ? null @@ -403,7 +406,7 @@ export function prepareNativeExecutable( } } timing("frontend-cache-miss"); - const tracker = new FrontendInputTracker(); + const tracker = new FrontendInputTracker(previous); const prepared = irInput !== "" ? preparedFromIr(irInput, entry, options) @@ -449,7 +452,10 @@ export function prepareNativeExecutable( llvm, ir, sidecarJson: null, - features: executableLinkFeatures(prepared.mod), + features: executableLinkFeatures( + prepared.mod, + options.largePages === true && outputKind === "exe", + ), sources: [...prepared.sourceTexts], ...(prepared.warnings.length > 0 ? { warnings: prepared.warnings } : {}), }; diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index df2bdf69b..c4b1bd651 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, + largePages: false, + utilTiming: false, + dynBuffer: false, + builtinValues: false, + computedRequire: false, + warningReplace: false, + stdioExtended: 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", () => { @@ -72,76 +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, - typeOrigins: false, - dynForAwait: false, - eventsOnceSignal: false, - mime: false, - utilTiming: false, - dynBuffer: false, - builtinValues: false, - computedRequire: false, - warningReplace: false, - stdioExtended: 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, - }, + features, }, }, directory, @@ -160,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 5d228a615..81667b28b 100644 --- a/packages/compiler/src/native/sanitizer.ts +++ b/packages/compiler/src/native/sanitizer.ts @@ -9,8 +9,12 @@ import { type NativeRuntimePack, type NativeRuntimeSelection, } from "../backend/runtime-pack-native.js"; -import { runtimePackFlavorKey, type RuntimePackMode } from "../backend/runtime-pack-core.js"; -import { LRE_SOURCES, ZLIB_SOURCES } from "../backend/vendor-inputs.js"; +import { + runtimePackFlavorKey, + selectedRuntimeUnits, + type RuntimePackMode, +} from "../backend/runtime-pack-core.js"; +import { LRE_SOURCES, zlibInputs } from "../backend/vendor-inputs.js"; import { contentDigest, type NativeCache } from "./cache.js"; import type { NativeToolchain } from "./toolchain.js"; @@ -127,7 +131,6 @@ export function buildSanitizedRuntime( 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", @@ -147,18 +150,14 @@ export function buildSanitizedRuntime( 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) @@ -172,9 +171,10 @@ export function buildSanitizedRuntime( directory = quickjs; args = ["-std=c11", "-I", quickjs]; } else if (name === "zlib") { - sources = ZLIB_SOURCES; + const inputs = zlibInputs(toolchain.target.llvmTriple); + sources = inputs.sources; directory = zlib; - args = ["-std=c11", "-I", zlib]; + args = ["-std=c11", ...inputs.defines.map((define) => "-D" + define), "-I", zlib]; } else if (name === "mbedtls") { directory = join(mbedtls, "library"); sources = readdirSync(directory) diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index c64bce76b..0acd4a4f5 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -276,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 c9167ba14..d93cce1af 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -44,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[]; @@ -69,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); @@ -96,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}`); } @@ -119,6 +137,8 @@ export function loadNativeToolchain( const manifest = raw as NativeToolchainManifest; for (const name of [ "tsc_ts", + "llvm_package", + "runtime_pack", "runtime_sources", "declarations", "archiver", @@ -157,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); @@ -206,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) }), diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts index f12a8ea89..4a479b597 100644 --- a/packages/compiler/src/project-config.test.ts +++ b/packages/compiler/src/project-config.test.ts @@ -70,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", @@ -78,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: { diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts index b87b67838..b4a7ac6fa 100644 --- a/packages/compiler/src/project-config.ts +++ b/packages/compiler/src/project-config.ts @@ -10,7 +10,8 @@ * "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"], @@ -40,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; @@ -117,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", @@ -129,6 +136,7 @@ const NODE_ISLANDS_KEYS = [ function defaultPackageSetting(): PackageSetting { return { unloaded: false, + shapes: true, run: "auto", evaluate: "auto", share: "auto", @@ -223,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; } @@ -236,12 +244,24 @@ 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)) { + 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; @@ -249,7 +269,7 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi 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; diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index 5a967cc5f..cb575e97c 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -9,13 +9,19 @@ afterEach(async () => { await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); -async function emit(source: string): Promise { +async function emit(source: string, optimization?: "dev"): Promise { const outDir = await mkdtemp(join(tmpdir(), "scriptc-await-call-")); 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" }); + const result = await compile(entry, { + entry, + outDir, + outPath, + outputKind: "llvm", + ...(optimization !== undefined ? { optimization } : {}), + }); expect(result.ok, JSON.stringify(result)).toBe(true); return readFile(outPath, "utf8"); } @@ -28,8 +34,9 @@ function definition(llvm: string, symbol: string): string { return match![0]; } -// Consuming an async generator keeps a body on its fiber: the await-call -// entries serve fiber bodies (stackless bodies suspend at their awaits). +// The await-call entries serve fiber bodies (stackless bodies suspend at +// their awaits). Debug builds keep every async body on its fiber, so the +// sources below that test them build with optimization "dev". const tick = ` async function* tick(): AsyncGenerator { yield 0; @@ -58,7 +65,7 @@ void main(); `; test("awaiting a direct call to a body that never suspends skips the promise", async () => { - const llvm = await emit(source); + const llvm = await emit(source, "dev"); expect(definition(llvm, "sc_f_middle")).toContain("call double @sc_aw_leaf(double"); const entry = definition(llvm, "sc_aw_leaf"); expect(entry).toContain("call double @sc_f_leaf(double %a0)"); @@ -74,7 +81,7 @@ test("awaiting a direct call to a body that never suspends skips the promise", a }); test("awaiting a direct call to a suspending body spawns and awaits in one call", async () => { - const llvm = await emit(source); + const llvm = await emit(source, "dev"); const main = definition(llvm, "sc_f_main"); expect(main).toContain("call double @sc_aw_middle(double"); // A call whose promise is kept keeps the promise-returning entry. @@ -82,7 +89,7 @@ test("awaiting a direct call to a suspending body spawns and awaits in one call" expect(definition(llvm, "sc_aw_middle")).toContain( "call double @scr_async_spawn_await_f64(ptr @sc_tr_middle, ptr %ap)", ); - // middle consumes an async generator, so it keeps its fiber. + // The debug build keeps middle on its fiber. expect(definition(llvm, "sc_as_middle")).toContain("@scr_async_spawn("); }); @@ -119,7 +126,7 @@ void main(); `; test("returning a direct call adopts its promise one microtask turn later", async () => { - const llvm = await emit(adoptSource); + const llvm = await emit(adoptSource, "dev"); expect(definition(llvm, "sc_f_relay")).toContain("call double @sc_ad_leaf(double"); // A body that never suspends: the adoption's then-job turn, then the // settled promise's own turn, still without materializing the promise. @@ -141,7 +148,7 @@ test("returning a direct call adopts its promise one microtask turn later", asyn }); test("a returned promise is adopted after finally regions, outside catch handlers", async () => { - const llvm = await emit(adoptSource); + const llvm = await emit(adoptSource, "dev"); const guarded = definition(llvm, "sc_f_guarded"); // The call produces the promise; the finally body runs; only then the // adoption turn and the await. @@ -167,7 +174,7 @@ async function middle(x: number): Promise { const z = await leaf(y); return y * 2 + z; } -async function fibered(): Promise { +async function fibered(): Promise { let s = 0; for await (const t of tick()) s += t; return s + (await middle(1)); @@ -206,8 +213,9 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin expect(middle).not.toContain("@scr_await_f64("); expect(definition(llvm, "sc_as_middle")).toContain("@scr_async_spawn_stackless("); expect(definition(llvm, "sc_f_main")).toContain("@scr_coro_await_prepare("); - // A body that keeps its fiber still awaits a stackless callee in one - // call: spawn, then the ordinary await of its promise. + // A body with a dynamic result keeps its fiber, and still awaits a + // stackless callee in one call: spawn, then the ordinary await of its + // promise. expect(definition(llvm, "sc_as_fibered")).toContain("@scr_async_spawn("); expect(definition(llvm, "sc_f_fibered")).toContain("call double @sc_aw_middle(double"); const entry = definition(llvm, "sc_aw_middle"); diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index c0a78a898..1998ff0af 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -404,7 +404,7 @@ test("large record clones stay outlined while small clones remain inlineable", ( expect(small).toContain("define internal ptr @sc_rclone_small(ptr %src) #0"); expect(small).not.toContain("attributes #2"); expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2"); - expect(large).toContain("attributes #2 = { noinline sanitize_address }"); + expect(large).toContain("attributes #2 = { noinline sanitize_address uwtable }"); }); test("Uint8ClampedArray stores clamp and round without a runtime call", () => { 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/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/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index d7ebc97fa..914dc81d8 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -137,6 +137,59 @@ test("worker programs poll counted loops over typed arrays between blocks of ite } }); +test("ELF worker executables poll the loops of a nest on every iteration", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-nest-poll-")); + try { + const entry = join(dir, "nest.ts"); + await writeFile( + entry, + [ + 'import { isMainThread, Worker } from "node:worker_threads";', + "export function steps(limit: number): number {", + " let total = 0;", + " for (let start = 1; start < limit; start++) {", + " let x = start;", + " while (x !== 1) {", + " x = x % 2 === 0 ? x / 2 : 3 * x + 1;", + " total++;", + " }", + " }", + " return total;", + "}", + "export function mix(n: number): number {", + " let t = 0;", + " for (let i = 0; i < n; i++) t = (t * 31 + i) % 1000003;", + " return t;", + "}", + "console.log(steps(8), mix(8));", + "if (!isMainThread) new Worker(new URL(import.meta.url));", + ].join("\n"), + ); + const outPath = join(dir, "nest.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 mod = deserializeModule(await readFile(outPath, "utf8")); + const body = (ll: string, name: string): string => + ll.match( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + ), + )![1]!; + // Both loops of the nest test the thread's pending word on every + // iteration and keep no countdown; a loop on its own keeps one. + const elf = emitLlvmModule(mod, { targetTriple: "x86_64-unknown-linux-gnu" }); + const steps = body(elf, "steps"); + expect(steps).not.toContain("worker cancellation budget"); + expect(steps.match(/= load atomic i32, ptr @scr_exc_flag monotonic/g)?.length).toBe(2); + expect(body(elf, "mix")).toContain("worker cancellation budget"); + // Elsewhere the poll reads the process-wide alert word: the nest counts. + const darwin = emitLlvmModule(mod, { targetTriple: "arm64-apple-macosx14.0.0" }); + expect(body(darwin, "steps").match(/worker cancellation budget/g)?.length).toBe(2); + } 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 { @@ -244,3 +297,144 @@ test("worker programs poll no loop whose proven ranges bound its runs inside ano 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 index 170c28ede..43d80b335 100644 --- a/packages/compiler/test/cpu-dispatch-emission.test.ts +++ b/packages/compiler/test/cpu-dispatch-emission.test.ts @@ -119,10 +119,10 @@ test("a dispatching x86-64 build gives each program function a baseline and an x 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); + expect(ll).toMatch(/^attributes #3 = \{ sanitize_address uwtable "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, + /^attributes #3 = \{ uwtable "target-cpu"="x86-64-v3" \}$/m, ); // main sets the level before anything else runs. expect(defs.get("main")).toMatch( 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/forin-keys.test.ts b/packages/compiler/test/forin-keys.test.ts index b35ff2154..4a8d24c7e 100644 --- a/packages/compiler/test/forin-keys.test.ts +++ b/packages/compiler/test/forin-keys.test.ts @@ -36,7 +36,7 @@ console.log(count(row), count(JSON.parse('{"b":1}'))); expect(llvm).not.toContain("@scr_dyn_for_in_keys("); // Fully required record shapes are marked fixed for the stable walk. expect(llvm).toMatch( - /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{[^}]*i8 1, ptr null, ptr null \}/, + /_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr, ptr \} \{[^}]*i8 1, ptr null, ptr null, ptr null \}/, ); }); 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 47e5f16c5..2442c5e09 100644 --- a/packages/compiler/test/generator-emission.test.ts +++ b/packages/compiler/test/generator-emission.test.ts @@ -9,13 +9,19 @@ afterEach(async () => { await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); -async function emit(source: string): Promise { +async function emit(source: string, optimization?: "dev"): Promise { const outDir = await mkdtemp(join(tmpdir(), "scriptc-generator-")); 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" }); + const result = await compile(entry, { + entry, + outDir, + outPath, + outputKind: "llvm", + ...(optimization !== undefined ? { optimization } : {}), + }); expect(result.ok, JSON.stringify(result)).toBe(true); return readFile(outPath, "utf8"); } @@ -50,7 +56,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 = { presplitcoroutine }"); + expect(llvm).toContain("attributes #1 = { uwtable presplitcoroutine }"); expect(definition(llvm, "sc_gs_range")).toContain("call ptr @scr_gen_new_stackless("); expect(llvm).not.toContain("@scr_gen_yield_f64("); }); @@ -117,7 +123,7 @@ console.log(sum, r.value); expect(llvm).toMatch(/define internal ptr @sc_gres_\d+\(ptr %g\)[^\n]*\(dyn channel\)/); }); -test("async generators keep their fibers", async () => { +test("async generators that suspend only at their own awaits and yields compile to stackless coroutines", async () => { const llvm = await emit(` async function* ticks(n: number): AsyncGenerator { for (let i = 0; i < n; i++) { @@ -130,6 +136,36 @@ async function main(): Promise { } void main(); `); + const body = definition(llvm, "sc_f_ticks"); + expect(body).toMatch(/^define internal void @sc_f_ticks(?:\.i32)?\(double [^)]*\) #1 /); + // The await suspends the coroutine (one turn for a non-promise operand), + // and the yield stores OUT before its own suspension. + expect(body).toContain("call zeroext i1 @scr_coro_hop_prepare(ptr"); + expect(body).toContain("call void @scr_coro_yield_f64(ptr"); + expect(body).toContain("call void @scr_gen_coro_finish(ptr"); + expect(body).not.toContain("@scr_await_hop("); + expect(body).not.toContain("@scr_gen_yield_f64("); + expect(definition(llvm, "sc_gs_ticks")).toContain("call ptr @scr_async_gen_new_stackless("); + // Consuming the generator does not park its consumer. + expect(definition(llvm, "sc_as_main")).toContain("@scr_async_spawn_stackless("); +}); + +test("debug builds keep async generators on their fibers", async () => { + const llvm = await emit( + ` +async function* ticks(n: number): AsyncGenerator { + for (let i = 0; i < n; i++) { + await null; + yield i; + } +} +async function main(): Promise { + for await (const t of ticks(3)) console.log(t); +} +void main(); +`, + "dev", + ); expect(definition(llvm, "sc_gs_ticks")).toContain("call ptr @scr_async_gen_new("); expect(llvm).not.toContain("presplitcoroutine"); }); 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/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index b350c743d..aa4d83959 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -50,6 +50,10 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); ); expect(body(llvm, "read")).toContain("@scr_js_get_probe("); expect(body(llvm, "write")).toContain("@scr_js_store_probe("); + // Reads and writes test the first two entries inline: the second + // starts 40 bytes into the record. + for (const site of ["read", "write"]) + expect(body(llvm, site)).toMatch(/getelementptr inbounds i8, ptr @sc_js_ic_\d+, i64 40$/m); expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); expect(body(llvm, "call")).toContain("@scr_js_load_probe("); // A compound assignment stores its number result from the jsval: an @@ -139,6 +143,50 @@ console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "ta } }); +test("property reads answer class instances' declared fields inline", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-capsule-")); + try { + const entry = join(dir, "main.js"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + ` +class Point { + constructor(x) { this.x = x; } +} +class Tagged extends Point { + constructor(x, tag) { super(x); this.tag = tag; } +} +class Gauge { + get x() { return 9; } +} +function dyn(v) { return v; } +function read(o) { return o.x; } +function tag(o) { return o.tag; } +var p = dyn(new Point(4)); +var t = dyn(new Tagged(6, "t")); +console.log(read(p), read(t), read(dyn(new Gauge())), read({ x: 1 }), tag(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(validateModule(mod)).toEqual([]); + const llvm = emitLlvmModule(mod); + const read = body(llvm, "read"); + // Point and Tagged instances load the field after one key compare + // each; Gauge's getter, and every other receiver, keeps the probe and + // the helper. + expect(read.match(/^icc\.m\d+:/gm)).toHaveLength(2); + expect(read).toContain("@scr_js_get_probe("); + expect(read).toMatch(/@sc_b?f__x25_dyn_class_property_\d+\(ptr %t\d+\) noinline cold/); + expect(body(llvm, "tag").match(/^icc\.m\d+:/gm)).toHaveLength(1); + } 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 { diff --git a/packages/compiler/test/large-pages.test.ts b/packages/compiler/test/large-pages.test.ts new file mode 100644 index 000000000..d3cbb4359 --- /dev/null +++ b/packages/compiler/test/large-pages.test.ts @@ -0,0 +1,37 @@ +import { execFileSync } 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"; + +// --large-pages links the runtime unit that moves the executable's code onto +// transparent huge pages (scr_text_pages.c); without the flag nothing of it +// is linked. A program too small to span an aligned 2 MiB page keeps its +// code where it is and runs as before. +test.skipIf(process.platform !== "linux")( + "--large-pages links the code-page unit only when requested", + async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-large-pages-")); + try { + const entry = join(dir, "main.ts"); + writeFileSync(entry, `console.log("pages", [1, 2, 3].map((n) => n * 2).join(","));\n`); + const symbols = async (largePages: boolean): Promise => { + const exe = join(dir, largePages ? "large" : "plain"); + const built = await compile(entry, { + outputKind: "exe", + outDir: dir, + outPath: exe, + ...(largePages ? { largePages: true } : {}), + }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("pages 2,4,6\n"); + return execFileSync("nm", [exe], { encoding: "utf8" }); + }; + expect(await symbols(false)).not.toContain("scr_text_large_pages"); + expect(await symbols(true)).toContain("scr_text_large_pages"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, +); 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/loop-entry-versions.test.ts b/packages/compiler/test/loop-entry-versions.test.ts new file mode 100644 index 000000000..22e41e3e7 --- /dev/null +++ b/packages/compiler/test/loop-entry-versions.test.ts @@ -0,0 +1,45 @@ +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 { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +test("while loops test their entry values and run integer remainders in the fast copy", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-entry-versions-")); + try { + const entry = join(import.meta.dirname, "../../../tests/corpus/while-entry-range-versions.ts"); + const outPath = join(dir, "versions.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 mod = deserializeModule(await readFile(outPath, "utf8")); + const ll = emitLlvmModule(mod, { pointerBits: 64 }); + const body = (name: string): string => + ll.match( + new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\{([\\s\\S]*?)\\n\\}`), + )![1]!; + for (const name of ["wrap", "ring", "_x25_Cursor_stripes"]) { + const fn = body(name); + const fast = fn.slice(fn.indexOf("\nentry.fast"), fn.indexOf("\nentry.slow")); + const slow = fn.slice(fn.indexOf("\nentry.slow")); + // The fast copy adds the dividend's integers; the ordinary copy adds + // doubles and tests the sum before its integer remainder. + const adds = (text: string): number => text.split("fadd double").length - 1; + expect(fast, name).toContain("srem i64"); + expect(adds(fast), name).toBeLessThan(adds(slow)); + } + // `complement` has no other caller: the fast copy's call keeps it inlined. + const wrap = body("wrap"); + const fast = wrap.slice(wrap.indexOf("\nentry.fast"), wrap.indexOf("\nentry.slow")); + expect(fast).toMatch(/call i32 @sc_f_complement\.i32\([^)]*\) alwaysinline/); + expect(wrap.slice(wrap.indexOf("\nentry.slow"))).not.toContain("alwaysinline"); + // Debug builds keep one copy. + const debug = emitLlvmModule(mod, { + pointerBits: 64, + debugSources: new Map([[entry, await readFile(entry, "utf8")]]), + }); + expect(debug).not.toContain("entry.fast"); + } finally { + await rm(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 bdad62203..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_v46"); + 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_v46"); + 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-partitions.test.ts b/packages/compiler/test/native-partitions.test.ts index fcd0132e8..18d0aa749 100644 --- a/packages/compiler/test/native-partitions.test.ts +++ b/packages/compiler/test/native-partitions.test.ts @@ -85,7 +85,13 @@ function expectedExit(edit = 0, count = 24): number { async function emit( dir: string, source: string, - options: { outputs: number; optLevel: "0" | "2"; cache?: string; name: string }, + options: { + outputs: number; + optLevel: "0" | "2"; + cache?: string; + split?: boolean; + name: string; + }, ): Promise { const input = join(dir, `${options.name}.ll`); await writeFile(input, source); @@ -110,6 +116,7 @@ async function emit( "--source-path", "/src/program.ts", ...(options.cache === undefined ? [] : ["--cache-dir", options.cache]), + ...(options.split === true ? ["--split", "codegen"] : []), ]); return outputs; } @@ -162,6 +169,191 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { }); } + test("split code generation links deterministically and defines every function once", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-split-codegen-")); + dirs.push(dir); + const parts = await emit(dir, program(), { + outputs: 3, + optLevel: "2", + split: true, + name: "split", + }); + expect(await exitCode(dir, parts, "split")).toBe(expectedExit()); + const again = await emit(dir, program(), { + outputs: 3, + optLevel: "2", + split: true, + name: "again", + }); + const contents = (paths: string[]) => Promise.all(paths.map((path) => readFile(path))); + expect(await contents(again)).toEqual(await contents(parts)); + + // The module is optimized whole, so the parts define the same functions as + // a single object, each in one part. + const defined = async (object: string) => { + const { stdout } = await execFileAsync("nm", ["--defined-only", object]); + return stdout + .split("\n") + .map((line) => /^[0-9a-fA-F]+ [Tt] _?(\w+)$/.exec(line.trim())?.[1]) + .filter((name): name is string => name !== undefined); + }; + const whole = await defined( + (await emit(dir, program(), { outputs: 1, optLevel: "2", name: "whole" }))[0]!, + ); + const split = await Promise.all(parts.map(defined)); + expect(split.flat().sort()).toEqual([...whole].sort()); + for (const names of split) expect(names.length).toBeGreaterThan(0); + }); + + test("split code generation keeps local a definition that only its own part uses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-split-codegen-local-")); + dirs.push(dir); + const count = 24; + const lines = ["@seed = internal global i64 5", "@total = internal global i64 0"]; + for (let index = 0; index < count; index++) + lines.push( + `define internal i64 @leaf${index}(i64 %value) noinline {`, + ` %scaled = mul i64 %value, ${index + 3}`, + " ret i64 %scaled", + "}", + `define internal void @step${index}(i64 %value) noinline {`, + ` %scaled = call i64 @leaf${index}(i64 %value)`, + " %old = load i64, ptr @total", + " %new = add i64 %old, %scaled", + " store i64 %new, ptr @total", + " ret void", + "}", + ); + lines.push( + "define i32 @main() {", + " %seed = load volatile i64, ptr @seed", + ...Array.from({ length: count }, (_, index) => ` call void @step${index}(i64 %seed)`), + " %total = load i64, ptr @total", + " %low = urem i64 %total, 251", + " %code = trunc i64 %low to i32", + " ret i32 %code", + "}", + ); + const parts = await emit(dir, lines.join("\n") + "\n", { + outputs: 3, + optLevel: "2", + split: true, + name: "local", + }); + let sum = 0; + for (let index = 0; index < count; index++) sum += 5 * (index + 3); + expect(await exitCode(dir, parts, "local")).toBe(sum % 251); + const symbols = await Promise.all( + parts.map(async (part) => (await execFileAsync("nm", ["--defined-only", part])).stdout), + ); + const bindings = (listing: string, prefix: string) => + [...listing.matchAll(new RegExp(`^[0-9a-fA-F]+ ([Tt]) _?${prefix}\\d+$`, "gm"))].map( + (match) => match[1], + ); + // A step in another part than main's is called across parts. A leaf is + // called only by its step, which is in the same part unless a part + // boundary falls between them. + const mainPart = symbols.findIndex((listing) => / T _?main$/m.test(listing)); + expect(mainPart).toBeGreaterThan(0); + const crossPart = symbols + .filter((_, index) => index !== mainPart) + .flatMap((listing) => bindings(listing, "step")); + expect(crossPart.length).toBeGreaterThan(0); + expect(new Set(crossPart)).toEqual(new Set(["T"])); + const leaves = symbols.flatMap((listing) => bindings(listing, "leaf")); + expect(leaves).toHaveLength(count); + expect(leaves.filter((bound) => bound === "t").length).toBeGreaterThanOrEqual(count - 2); + }); + + test("split code generation links a thread-local global that only one part uses", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-split-codegen-tls-")); + dirs.push(dir); + // @touch and main reach @slots through one constant expression, which + // outlives their bodies in the part that holds neither. + const thread = [ + "@calls = internal thread_local(localexec) global i64 0", + "@slots = internal thread_local(localexec) global [2 x i64] zeroinitializer", + "define internal void @touch() noinline {", + " store volatile i64 1, ptr getelementptr inbounds (i8, ptr @slots, i64 8)", + " ret void", + "}", + ].join("\n"); + const source = program(0, thread).replace( + " %total = load i64, ptr @total", + [ + " store volatile i64 1, ptr @calls", + " call void @touch()", + " %slot = load volatile i64, ptr getelementptr inbounds (i8, ptr @slots, i64 8)", + " %total = load i64, ptr @total", + ].join("\n"), + ); + const parts = await emit(dir, source, { outputs: 3, optLevel: "2", split: true, name: "tls" }); + expect(await exitCode(dir, parts, "tls")).toBe(expectedExit()); + }); + + test("split code generation keeps a module that one function dominates whole", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-split-codegen-whole-")); + dirs.push(dir); + const body = Array.from( + { length: 400 }, + (_, index) => ` %v${index + 1} = mul i64 %v${index}, %v${index}`, + ); + const heavy = [ + "@sink = internal global i64 0", + "define internal void @heavy(i64 %v0) noinline {", + ...body, + " store volatile i64 %v400, ptr @sink", + " ret void", + "}", + ].join("\n"); + const source = program(0, heavy, 2).replace( + " %total = load i64, ptr @total", + " call void @heavy(i64 %second)\n %total = load i64, ptr @total", + ); + const parts = await emit(dir, source, { + outputs: 3, + optLevel: "2", + split: true, + name: "whole", + }); + expect(await exitCode(dir, parts, "whole")).toBe(expectedExit(0, 2)); + const { stdout } = await execFileAsync("nm", ["--defined-only", parts[0]!]); + expect(stdout).toMatch(/ t _?heavy$/m); + for (const part of parts.slice(1)) { + const listed = await execFileAsync("nm", ["--defined-only", part]); + expect(listed.stdout.trim()).toBe(""); + } + // The first output is the object that a single output produces. + const first = await readFile(parts[0]!); + const [single] = await emit(dir, source, { outputs: 1, optLevel: "2", name: "whole" }); + expect(await readFile(single!)).toEqual(first); + // The empty parts name the source path, so they do not depend on the + // input's file name either. + const renamed = await emit(dir, source, { + outputs: 3, + optLevel: "2", + split: true, + name: "renamed", + }); + for (let index = 0; index < parts.length; index++) + expect(await readFile(renamed[index]!)).toEqual(await readFile(parts[index]!)); + }); + + test("split code generation requires several optimized outputs and no cache", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-split-codegen-invalid-")); + dirs.push(dir); + for (const options of [ + { outputs: 1, optLevel: "2" }, + { outputs: 3, optLevel: "0" }, + { outputs: 3, optLevel: "2", cache: join(dir, "cache") }, + ] as const) { + const failure = emit(dir, program(), { ...options, split: true, name: "invalid" }); + await expect(failure).rejects.toMatchObject({ + stderr: expect.stringContaining("invalid_split"), + }); + } + }); + test("functions under one group line share a partition", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-partitions-groups-")); dirs.push(dir); @@ -204,12 +396,111 @@ describe.runIf(helper !== null)("LLVM program partitions", () => { 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. - const contents = (paths: string[]) => Promise.all(paths.map((path) => readFile(path))); + // 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; - expect( - await contents(await emit(dir, grouped(0, 24), { ...few, name: "few-grouped" })), - ).toEqual(await contents(await emit(dir, program(), { ...few, name: "few-plain" }))); + 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 () => { 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 4d4c4a7e8..f13c5558e 100644 --- a/packages/compiler/test/node-pin.test.ts +++ b/packages/compiler/test/node-pin.test.ts @@ -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/offset-index-emission.test.ts b/packages/compiler/test/offset-index-emission.test.ts new file mode 100644 index 000000000..c8bb49c8b --- /dev/null +++ b/packages/compiler/test/offset-index-emission.test.ts @@ -0,0 +1,63 @@ +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"; + +function functionBody(llvm: string, name: string): string { + const start = llvm.indexOf(name); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); +} + +async function emitLlvm(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-offset-index-")); + 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("typed array accesses at a number base plus an integer convert the base, not the sum", async () => { + const llvm = await emitLlvm(` +function window(values: Float64Array, at: number): number { + return values[at + 0]! + values[at + 1]! + values[2 + at]! + values[at - 1]!; +} +const values = new Float64Array([1, 2, 3, 4, 5, 6]); +console.log(window(values, Number(process.argv[2] ?? "1"))); +`); + const body = functionBody(llvm, "@sc_bf_window("); + const sums = new Set([...body.matchAll(/(%t\d+) = fadd double/g)].map((m) => m[1])); + // Each access converts the base and adds its offset as an integer. Only + // a fractional base reaches the exact check of the sum itself. + const fast = body + .split(/\n(?=[\w.]+:)/) + .filter((block) => !block.startsWith("bytes.index.sum")) + .join("\n"); + const converted = [...fast.matchAll(/fptosi double (%[\w.]+) to i64/g)].map((m) => m[1]); + expect(converted.length).toBeGreaterThanOrEqual(4); + expect(converted.filter((name) => sums.has(name))).toEqual([]); + for (const offset of [1, 2, -1]) + expect(body).toMatch(new RegExp(`= add i64 %t\\d+, ${offset}\\n`)); +}); + +test("a typed array write converts the base its index sum read, before the right-hand side", async () => { + const llvm = await emitLlvm(` +function rebase(values: Int32Array, at: number): number { + values[at + 1] = ((at = 5), 31); + return at; +} +const values = new Int32Array(8); +console.log(rebase(values, Number(process.argv[2] ?? "2")), values.join(",")); +`); + const body = functionBody(llvm, "@sc_bf_rebase("); + const base = /= fadd double (%[\w.]+), /.exec(body)?.[1]; + expect(base).toBeDefined(); + expect(body).toContain(`fptosi double ${base} to i64`); +}); 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/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/source-output.test.ts b/packages/compiler/test/source-output.test.ts index ce411f102..f61ce61e5 100644 --- a/packages/compiler/test/source-output.test.ts +++ b/packages/compiler/test/source-output.test.ts @@ -60,6 +60,19 @@ test("detached exit refuses non-executable compiler output", async () => { await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" }); }); +test("large pages refuse non-executable compiler output", async () => { + const { entry, outDir } = await fixture(); + const outPath = join(outDir, "main.ll"); + const result = await compile(entry, { outDir, outPath, outputKind: "llvm", largePages: true }); + expect(result).toMatchObject({ + ok: false, + diagnostics: [ + { code: "SC3002", message: expect.stringContaining("only supported for executable output") }, + ], + }); + await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" }); +}); + test("LLVM is the exact primary artifact and never creates an executable", async () => { const { entry, outDir } = await fixture(); const llvmPath = join(outDir, "exact.llvm-output"); diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index d26a0c96c..dcec6d8af 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -183,3 +183,47 @@ console.log(key(new Uint8Array([1, 2, 3, 0]), 6)); 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/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts index c82db039d..13ed000df 100644 --- a/packages/compiler/test/stable-names.test.ts +++ b/packages/compiler/test/stable-names.test.ts @@ -117,3 +117,44 @@ test("names do not depend on the directory the program sits in", async () => { const [second] = await lower(recursive); expect(names(second!)).toEqual(names(first!)); }); + +/** Lowers a program of several files; `main.ts` is the entry. */ +async function lowerFiles(files: Record): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); + try { + for (const [name, source] of Object.entries(files)) await writeFile(join(dir, name), source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(join(dir, "main.ts"), { 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 }); + } +} + +test("a module's names do not depend on the modules loaded before it", async () => { + const shapes = ` +export class Box { + constructor(private value: number) {} + scaled(by: number): number { return this.value * by; } +} +export function boxes(count: number): number[] { + return Array.from({ length: count }, (_, i) => new Box(i).scaled(2)); +} +`; + const before = await lowerFiles({ + "shapes.ts": shapes, + "main.ts": `import { boxes } from "./shapes";\nconsole.log(boxes(3).join(","));\n`, + }); + const after = await lowerFiles({ + "offsets.ts": `export function offset(x: number): number { return x + 1; }\n`, + "shapes.ts": shapes, + "main.ts": `import { offset } from "./offsets";\nimport { boxes } from "./shapes";\nconsole.log(offset(1), boxes(3).join(","));\n`, + }); + // The class, its methods, the function and the closure it lifts. + const ofShapes = (mod: IrModule) => + names(mod).filter((name) => /Box|boxes/.test(name) || name.startsWith("%fn.")); + expect(ofShapes(before).length).toBeGreaterThanOrEqual(4); + expect(ofShapes(after)).toEqual(ofShapes(before)); +}); 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/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/7393-non-null-element-operands.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7393-non-null-element-operands.ts.json new file mode 100644 index 000000000..e50ca92f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7393-non-null-element-operands.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7393-non-null-element-operands.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json new file mode 100644 index 000000000..17551e626 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7395-non-null-element-dereference.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7395-non-null-element-dereference.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/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/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/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/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/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/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/8370-stack-overflow-cycles/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8370-stack-overflow-cycles/main.ts.json new file mode 100644 index 000000000..7e5125dde --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8370-stack-overflow-cycles/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8370-stack-overflow-cycles/shapes.ts", + "/tests/corpus/8370-stack-overflow-cycles/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8380-untyped-generator-loops.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8380-untyped-generator-loops.mjs.json new file mode 100644 index 000000000..91ef13397 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8380-untyped-generator-loops.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8380-untyped-generator-loops.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8381-untyped-generator-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8381-untyped-generator-stages.mjs.json new file mode 100644 index 000000000..f272e7d88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8381-untyped-generator-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8381-untyped-generator-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8382-stackless-async-generator-steps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8382-stackless-async-generator-steps.mjs.json new file mode 100644 index 000000000..aa47502ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8382-stackless-async-generator-steps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8382-stackless-async-generator-steps.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8383-stackless-async-generator-steps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8383-stackless-async-generator-steps.ts.json new file mode 100644 index 000000000..2c9f5537a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8383-stackless-async-generator-steps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8383-stackless-async-generator-steps.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/8405-zlib-compressed-bytes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8405-zlib-compressed-bytes.ts.json new file mode 100644 index 000000000..509b30d34 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8405-zlib-compressed-bytes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8405-zlib-compressed-bytes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8406-zlib-inflate-chunks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8406-zlib-inflate-chunks.cjs.json new file mode 100644 index 000000000..772d91511 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8406-zlib-inflate-chunks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8406-zlib-inflate-chunks.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8407-string-index-of-from-ascii.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8407-string-index-of-from-ascii.ts.json new file mode 100644 index 000000000..15651f491 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8407-string-index-of-from-ascii.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8407-string-index-of-from-ascii.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8410-dynamic-class-instance-reads.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8410-dynamic-class-instance-reads.js.json new file mode 100644 index 000000000..cec5ee64b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8410-dynamic-class-instance-reads.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8410-dynamic-class-instance-reads.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8411-dynamic-base-keyed-instances/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8411-dynamic-base-keyed-instances/main.ts.json new file mode 100644 index 000000000..5d15c0479 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8411-dynamic-base-keyed-instances/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/8411-dynamic-base-keyed-instances/reader.cjs", + "/tests/corpus/8411-dynamic-base-keyed-instances/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-array-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-array-subclass.ts.json new file mode 100644 index 000000000..84bff57e0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-array-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-array-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-arrays.ts.json new file mode 100644 index 000000000..eafbe937c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-arrays.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-cap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-cap.ts.json new file mode 100644 index 000000000..70593bc13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-cap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-cap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-escapes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-escapes.ts.json new file mode 100644 index 000000000..402b73088 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-escapes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-escapes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-mixed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-mixed.ts.json new file mode 100644 index 000000000..17269a5b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-mixed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-mixed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-outside.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-outside.ts.json new file mode 100644 index 000000000..59a1a597c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-outside.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-outside.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-region-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-region-throws.ts.json new file mode 100644 index 000000000..392abe549 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-region-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-region-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-regions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-regions.ts.json new file mode 100644 index 000000000..7f2425ffc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-regions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-regions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-thrown-member.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-thrown-member.ts.json new file mode 100644 index 000000000..8657adc5c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-thrown-member.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-thrown-member.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-throws.ts.json new file mode 100644 index 000000000..c21f13a02 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-trees.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-trees.ts.json new file mode 100644 index 000000000..9d9b75df2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-trees.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-trees.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-workers.ts.json new file mode 100644 index 000000000..97915bb85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/arena-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/arena-workers.ts" + ], + "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/array-subclass-element-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-subclass-element-cycles.ts.json new file mode 100644 index 000000000..136c92da1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-subclass-element-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/array-subclass-element-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-subclass-element-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-subclass-element-instanceof.ts.json new file mode 100644 index 000000000..0a542048a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/array-subclass-element-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/array-subclass-element-instanceof.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/discarded-logical-void-conditionals.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/discarded-logical-void-conditionals.mjs.json new file mode 100644 index 000000000..837cbad85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/discarded-logical-void-conditionals.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/discarded-logical-void-conditionals.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/discarded-logical-void-conditionals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/discarded-logical-void-conditionals.ts.json new file mode 100644 index 000000000..79c4868ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/discarded-logical-void-conditionals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/discarded-logical-void-conditionals.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/frozen-record-untyped-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/frozen-record-untyped-writes.ts.json new file mode 100644 index 000000000..800f25e9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/frozen-record-untyped-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/frozen-record-untyped-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/frozen-untyped-writes-sloppy.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/frozen-untyped-writes-sloppy.cjs.json new file mode 100644 index 000000000..e8af9b305 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/frozen-untyped-writes-sloppy.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/frozen-untyped-writes-sloppy.cjs" + ], + "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/integer-clone-int32-slots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/integer-clone-int32-slots.ts.json new file mode 100644 index 000000000..e26a5a727 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/integer-clone-int32-slots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/integer-clone-int32-slots.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-degraded-initializer.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-degraded-initializer.ts.json new file mode 100644 index 000000000..90cdeee2d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-degraded-initializer.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-degraded-initializer.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-expressions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-expressions.ts.json new file mode 100644 index 000000000..97f6c7ce3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-expressions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-expressions.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-push-down.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-push-down.ts.json new file mode 100644 index 000000000..554c74bac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/localized-failure-push-down.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/localized-failure-push-down.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/native-worker-terminate-short-inner-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-short-inner-loops.ts.json new file mode 100644 index 000000000..8332ee20d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-short-inner-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-short-inner-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/non-null-element-computed-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/non-null-element-computed-index.ts.json new file mode 100644 index 000000000..3030323af --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/non-null-element-computed-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/non-null-element-computed-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-dynamic-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-dynamic-writes.ts.json new file mode 100644 index 000000000..8de4b7d03 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-dynamic-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/record-dynamic-writes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-prefix-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-prefix-identity.ts.json new file mode 100644 index 000000000..273bff2c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/record-prefix-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/record-prefix-identity.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/short-char-codes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/short-char-codes.ts.json new file mode 100644 index 000000000..79390f594 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/short-char-codes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/short-char-codes.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/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/string-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json new file mode 100644 index 000000000..2c5cc69e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/string-spread.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/typed-array-nested-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-nested-loop-versions.ts.json new file mode 100644 index 000000000..66c967b58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-nested-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-nested-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-offset-indexes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-offset-indexes.ts.json new file mode 100644 index 000000000..52beff510 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-offset-indexes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/typed-array-offset-indexes.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-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-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/untyped-nullish-scalar-defaults.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/untyped-nullish-scalar-defaults.mjs.json new file mode 100644 index 000000000..2bce0e918 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/untyped-nullish-scalar-defaults.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/untyped-nullish-scalar-defaults.mjs" + ], + "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/while-entry-range-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/while-entry-range-versions.ts.json new file mode 100644 index 000000000..ec9e8eeae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/while-entry-range-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/while-entry-range-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json new file mode 100644 index 000000000..543e1f640 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/width-layout-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json new file mode 100644 index 000000000..6aa2085bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/width-layout-parameter-writes.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/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/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/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/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/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/untyped-nullish-scalar.test.ts b/packages/compiler/test/untyped-nullish-scalar.test.ts new file mode 100644 index 000000000..c9e496b88 --- /dev/null +++ b/packages/compiler/test/untyped-nullish-scalar.test.ts @@ -0,0 +1,46 @@ +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"; + +test("`??` in JavaScript tests a scalar optional left on the typed path when the default has its type", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-untyped-nullish-")); + try { + const entry = join(dir, "main.mjs"); + writeFileSync( + entry, + [ + "const counts = new Map();", + 'for (const w of ["a", "b", "a"]) counts.set(w, (counts.get(w) ?? 0) + 1);', + 'console.log(counts.get("a") ?? 0, counts.get("z") ?? "none");', + ].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)); + const module = deserializeModule(readFileSync(output, "utf8")); + expect(validateModule(module)).toEqual([]); + const nullish: { type: string; left: string }[] = []; + const visit = (node: unknown, parent: string | null): void => { + if (node === null || typeof node !== "object") return; + if (Array.isArray(node)) return node.forEach((child) => visit(child, parent)); + const n = node as { kind?: string; type?: { kind: string }; left?: { kind: string } }; + if (n.kind === "nullish") + nullish.push({ type: `${parent}>${n.type!.kind}`, left: n.left!.kind }); + for (const [key, value] of Object.entries(node)) + if (key !== "loc") visit(value, n.kind ?? parent); + }; + visit(module.functions, null); + // The loop's `?? 0`, whose result is an untyped value, reads the Map's + // number on the typed path and converts only the result. (The first + // console.log argument is a number already.) `?? "none"` takes another + // type, so its left converts first. + expect( + nullish.filter((n) => n.type === "dynFrom>f64" && n.left === "mapIntrinsic"), + ).toHaveLength(1); + expect(nullish.filter((n) => n.type.endsWith(">dyn") && n.left === "dynFrom")).toHaveLength(1); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/worker-shared-loop-version-emission.test.ts b/packages/compiler/test/worker-shared-loop-version-emission.test.ts index 7eebcc2e4..ad7ec1ae9 100644 --- a/packages/compiler/test/worker-shared-loop-version-emission.test.ts +++ b/packages/compiler/test/worker-shared-loop-version-emission.test.ts @@ -56,11 +56,13 @@ if (isMainThread) { // 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, and shared-safe: a view - // with shared storage loads atomically. + // 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 atomic double, ptr %\w+ monotonic/); + 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 () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 6006e73ac..95fe91ab3 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -15,6 +15,8 @@ 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"; +import { zlibInputs } from "../../runtime-pack-common/scripts/zlib-inputs.mjs"; const run = promisify(execFile); const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); @@ -75,19 +77,7 @@ async function build() { 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 = [ - "adler32.c", - "compress.c", - "crc32.c", - "deflate.c", - "infback.c", - "inffast.c", - "inflate.c", - "inftrees.c", - "trees.c", - "uncompr.c", - "zutil.c", - ]; + const ZLIB_BUILD = zlibInputs(RUNTIME_PACK_MATRIX.target.llvm_triple); // Speed-flavor executable units are emitted through the host LLVM helper // so their object and import bitcode come from one promoted module. The @@ -308,11 +298,12 @@ async function build() { "-I", quickjs, ]), - await archive("zlib", ZLIB_SOURCES, zlib, [ + await archive("zlib", ZLIB_BUILD.sources, zlib, [ "-target", RUNTIME_PACK_MATRIX.target.llvm_triple, "-std=c11", - "-Os", + "-O3", + ...ZLIB_BUILD.defines.map((define) => `-D${define}`), ...codeAlignment, "-I", zlib, @@ -355,6 +346,7 @@ async function build() { ], [join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], + [join(zlib, "LICENSE.chromium"), "artifacts/licenses/zlib-chromium.txt", "BSD-3-Clause"], [join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"], ]; @@ -365,13 +357,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: 46, marker: "scr_runtime_abi_v46" }, + runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 16178e851..0f2a69c49 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -47,6 +47,7 @@ export const BASE_RUNTIME_SOURCES = [ "scr_cycle.c", "scr_publish.c", "scr_region.c", + "scr_arena.c", "scr_itable.c", "scr_slots.c", "scr_alloc.c", @@ -100,6 +101,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // events.once with options registers through the emitter and event-target units. ["scr_events_once_signal.c", "eventsOnceSignal"], ["scr_mime.c", "mime"], + // `scriptc build --large-pages`: executable code on transparent huge pages (Linux). + ["scr_text_pages.c", "largePages"], ["scr_util_timing.c", "utilTiming"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], @@ -187,9 +190,9 @@ export const OPTIONAL_RUNTIME_SOURCES = [ export function variantsFor(source) { const variants = [{ id: "default", when: {}, defines: [] }]; - // Programs with dynamic-tier code keep the tier's hidden classes current - // in checked objects (two-tier design S4); every other program links the - // ordinary object, byte for byte. + // Programs with dynamic-tier code link the build that exports the + // primitives the tier's inline caches use (two-tier design S4); every + // other program links the ordinary object, byte for byte. if (source === "scr_json.c") { variants.push({ id: "dyn-tier", when: { dynTier: true }, defines: ["SCR_JS_TIER"] }); } diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 357ba7337..aebf1b017 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -15,7 +15,9 @@ 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"; +import { zlibInputs } from "./zlib-inputs.mjs"; const run = promisify(execFile); const packageRoot = process.env.SCRIPTC_RUNTIME_PACK_ROOT; @@ -88,19 +90,7 @@ async function build() { const brotli = join(vendorRoot, "brotli", "c"); const qjsSources = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"]; const lreSources = ["libregexp.c", "libunicode.c"]; - const zlibSources = [ - "adler32.c", - "compress.c", - "crc32.c", - "deflate.c", - "infback.c", - "inffast.c", - "inflate.c", - "inftrees.c", - "trees.c", - "uncompr.c", - "zutil.c", - ]; + const zlibBuild = zlibInputs(config.target.llvm_triple); const sha256 = async (path) => createHash("sha256") .update(await readFile(path)) @@ -399,10 +389,11 @@ async function build() { : []), ...(requestedArchives.has("zlib") ? [ - await archive("zlib", zlibSources, zlib, [ + await archive("zlib", zlibBuild.sources, zlib, [ ...vendorTarget, "-std=c11", - "-Os", + "-O3", + ...zlibBuild.defines.map((define) => `-D${define}`), "-I", zlib, ]), @@ -453,6 +444,7 @@ async function build() { "BSD-3-Clause", ], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], + [join(zlib, "LICENSE.chromium"), "artifacts/licenses/zlib-chromium.txt", "BSD-3-Clause"], [join(vendorRoot, "brotli", "LICENSE"), "artifacts/licenses/brotli.txt", "MIT"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"], ]; @@ -468,13 +460,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: 46, marker: "scr_runtime_abi_v46" }, + 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/scripts/zlib-inputs.mjs b/packages/runtime-pack-common/scripts/zlib-inputs.mjs new file mode 100644 index 000000000..efb433ff8 --- /dev/null +++ b/packages/runtime-pack-common/scripts/zlib-inputs.mjs @@ -0,0 +1,83 @@ +// The translation units and defines of the vendored zlib (packages/runtime/vendor/zlib, +// the copy Node v24.22.0 bundles in deps/zlib), chosen per target the way Node's +// deps/zlib/zlib.gyp chooses them. The SIMD units carry their own target attributes +// and check the CPU at run time, so every unit builds with the same flags. Compressed +// output does not depend on these choices. packages/compiler/src/backend/vendor-inputs.ts +// keeps the same table for cross builds; a unit test checks that the two agree. + +const COMMON_SOURCES = [ + "adler32.c", + "compress.c", + "cpu_features.c", + "crc32.c", + "deflate.c", + "infback.c", + "inffast.c", + "inftrees.c", + "trees.c", + "uncompr.c", + "zutil.c", +]; + +/** + * @param {string} triple an LLVM or Zig target triple, such as + * "x86_64-unknown-linux-gnu", "arm64-apple-ios15.0" or "aarch64-linux-musl". + * @returns {{ sources: string[], defines: string[] }} + */ +export function zlibInputs(triple) { + const [cpu = "", ...rest] = triple.split("-"); + const system = rest.join("-"); + const architecture = + cpu === "x86_64" || cpu === "amd64" + ? "x64" + : cpu === "aarch64" || cpu === "arm64" + ? "arm64" + : cpu; + const ios = system.includes("ios"); + const windows = + system.includes("windows") || system.includes("win32") || system.includes("mingw"); + // Android reads the CPU features through getauxval, as Linux does: Node's + // ARMV8_OS_ANDROID path needs the NDK's cpu-features library. + const linux = system.includes("linux") || system.includes("android"); + const mac = !ios && (system.includes("darwin") || system.includes("macos")); + const sources = [...COMMON_SOURCES]; + const defines = windows ? [] : ["HAVE_HIDDEN"]; + if (architecture === "x64" && !ios) { + sources.push( + "adler32_simd.c", + "contrib/optimizations/inffast_chunk.c", + "contrib/optimizations/inflate.c", + ); + defines.push( + "ADLER32_SIMD_SSSE3", + windows ? "X86_WINDOWS" : "X86_NOT_WINDOWS", + "INFLATE_CHUNK_SIMD_SSE2", + "INFLATE_CHUNK_READ_64LE", + "DEFLATE_SLIDE_HASH_SSE2", + ); + } else if (architecture === "arm64") { + sources.push("contrib/optimizations/inffast_chunk.c", "contrib/optimizations/inflate.c"); + defines.push( + "INFLATE_CHUNK_SIMD_NEON", + "INFLATE_CHUNK_READ_64LE", + "__ARM_NEON__", + "DEFLATE_SLIDE_HASH_NEON", + ); + if (windows) { + defines.push("ARMV8_OS_WINDOWS"); + } else if (linux || mac) { + // Node builds the Adler-32 and CRC-32 units for every arm64 target but + // iOS and Windows. + sources.push("adler32_simd.c", "crc32_simd.c"); + defines.push( + "ADLER32_SIMD_NEON", + "CRC32_ARMV8_CRC32", + linux ? "ARMV8_OS_LINUX" : "ARMV8_OS_MACOS", + ); + } + } else { + sources.push("inflate.c"); + defines.push("CPU_NO_SIMD"); + } + return { sources, defines }; +} 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/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index a27df378a..ac80df6e2 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -368,6 +368,34 @@ static bool scr_sa_pf_on(void) { return s > 0; } +/* fork() copies only the calling thread, so a child has no helper. The + * prepare handler takes the lock, so no other thread holds it across the + * fork. The child handler gives the child fresh helper state: an empty ring, + * no busy range or waiters, new conditions (the parent's can still record + * the helper as a waiter), and the heap watermarks cleared. Its first + * request then starts a helper of its own. */ +static bool scr_sa_pf_atfork; + +static void scr_sa_pf_fork_prepare(void) { pthread_mutex_lock(&scr_sa_pf_lock); } + +static void scr_sa_pf_fork_parent(void) { pthread_mutex_unlock(&scr_sa_pf_lock); } + +static void scr_sa_pf_fork_child(void) { + (void)pthread_mutex_init(&scr_sa_pf_lock, NULL); + (void)pthread_cond_init(&scr_sa_pf_cond, NULL); + (void)pthread_cond_init(&scr_sa_pf_done, NULL); + scr_sa_pf_head = scr_sa_pf_tail = 0; + scr_sa_pf_busy = (ScrSaPfRange){NULL, NULL}; + scr_sa_pf_forgetting = 0; + scr_sa_pf_waiting = false; + scr_sa_pf_started = false; +#ifdef SCR_SA_PF_HEAP + scr_sa_pf_heap_mode = 0; + scr_sa_pf_brk0 = scr_sa_pf_brk = NULL; + scr_sa_pf_heap_big = false; +#endif +} + /* Asks the helper to fault in [lo, hi) (page aligned). Dropped when the * ring is full: the owner then faults those pages itself. */ static void scr_sa_prefault(char *lo, char *hi) { @@ -375,6 +403,14 @@ static void scr_sa_prefault(char *lo, char *hi) { pthread_mutex_lock(&scr_sa_pf_lock); if (!scr_sa_pf_started) { scr_sa_pf_started = true; + /* Once per process: a child inherits the parent's handlers. */ + if (!scr_sa_pf_atfork && + pthread_atfork(scr_sa_pf_fork_prepare, scr_sa_pf_fork_parent, scr_sa_pf_fork_child) != 0) { + pthread_mutex_unlock(&scr_sa_pf_lock); + atomic_store_explicit(&scr_sa_pf_state, -1, memory_order_relaxed); + return; + } + scr_sa_pf_atfork = true; #ifdef SCR_SA_PF_HEAP /* Nobody waits on the condition yet: give it the monotonic clock for * the helper's polls (no polls without it). */ @@ -2116,6 +2152,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 dfdd9bf5c..bf9fc7e64 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -1023,6 +1023,65 @@ static void check_discard(void) { } #endif +/* ThreadSanitizer stops a child that starts a thread after a fork in a + * threaded program, so its build skips the fork check. */ +#define FORK_CHECK 1 +#if defined(__SANITIZE_THREAD__) +#undef FORK_CHECK +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) +#undef FORK_CHECK +#endif +#endif + +#if defined(__linux__) && SCR_SMALL_ALLOC && defined(FORK_CHECK) +#include +#include +#include +#include +#include +#define FORK_ROUNDS 64 +#define FORK_SPAN ((size_t)64 << 20) +#define FORK_OWN ((size_t)1 << 20) + +/* A child forked while the prefault helper faults in a range has no helper. + * It releases that range (which waits for a busy range to finish), then + * requests a fault-in of its own and releases it, and must exit within five + * seconds. */ +static void check_fork_while_prefaulting(void) { + for (unsigned round = 0; round < FORK_ROUNDS; round++) { + char *lo = mmap(NULL, FORK_SPAN, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + assert(lo != MAP_FAILED); + scr_mem_prefault(lo, lo + FORK_SPAN); + pid_t pid = fork(); + assert(pid >= 0); + if (pid == 0) { + scr_mem_release_ahead(lo, lo + FORK_SPAN); + char *own = mmap(NULL, FORK_OWN, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (own == MAP_FAILED) _exit(2); + scr_mem_prefault(own, own + FORK_OWN); + scr_mem_release_ahead(own, own + FORK_OWN); + _exit(0); + } + int status = 0; + pid_t done = 0; + for (unsigned ms = 0; ms < 5000 && done == 0; ms++) { + done = waitpid(pid, &status, WNOHANG); + if (done == 0) nanosleep(&(struct timespec){0, 1000000L}, NULL); + } + if (done == 0) { + kill(pid, SIGKILL); + (void)waitpid(pid, &status, 0); + fprintf(stderr, "the child forked in round %u did not exit\n", round); + abort(); + } + assert(done == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0); + scr_mem_release_ahead(lo, lo + FORK_SPAN); + assert(munmap(lo, FORK_SPAN) == 0); + } +} +#endif + #if defined(__linux__) && SCR_SMALL_ALLOC /* The program-lifetime region hands out zeroed, 8-byte aligned blocks below * 32 GiB, in order from the calling thread's chunk; a block larger than a @@ -1087,6 +1146,9 @@ int main(void) { check_system_pointers(); check_churn(); check_free_order(); +#if defined(__linux__) && SCR_SMALL_ALLOC && defined(FORK_CHECK) + check_fork_while_prefaulting(); +#endif #if SCR_SMALL_ALLOC && defined(__linux__) && defined(__GLIBC__) check_heap_huge_pages(); #endif diff --git a/packages/runtime/src/scr_arena.c b/packages/runtime/src/scr_arena.c new file mode 100644 index 000000000..e04a6ed35 --- /dev/null +++ b/packages/runtime/src/scr_arena.c @@ -0,0 +1,263 @@ +/* Group arenas: see scr_arena.h. */ +#include "scr_runtime.h" + +#include +#include +#include + +/* The first chunk lives inside the arena, so a small group (one result + * record, a short builder) touches no other memory. */ +#define SCR_ARENA_INLINE 512 +#define SCR_ARENA_CHUNK_MIN ((size_t)4096) +#define SCR_ARENA_CHUNK_MAX ((size_t)1 << 20) +/* Bytes an arena that is not closed takes before its call allocates on the + * heap again: a group whose objects die before it is dropped holds at most + * this much dead memory. A closed arena has no cap: once on the heap, its + * later objects could come to be held by earlier ones, which are not on + * its dispose list. Neither do objects with a collector header: on the + * heap, they could form cycles that outlive the drop (llvm/arenas.ts). */ +#define SCR_ARENA_CAP ((size_t)32 << 20) +/* What a thread keeps for reuse: arenas, chunk bytes, and dispose list + * entries per pooled arena. */ +#define SCR_ARENA_POOL 64 +#define SCR_ARENA_POOL_BYTES ((size_t)8 << 20) +#define SCR_ARENA_POOL_DISPOSALS ((size_t)4096) + +/* A chunk's objects run from scr_arena_chunk_start to its end. */ +typedef struct ScrArenaChunk { + struct ScrArenaChunk *next; + size_t size; /* bytes, this header included */ +} ScrArenaChunk; + +/* The leading fields are an ABI: emitted arena-aware constructors bump + * `bump` up to `limit`, test `closed`, and append to the dispose list at + * `disposals` up to `disposals_end` inline (llvm/arenas.ts). Without the + * small-object allocator `bump` and `limit` stay NULL, so every object + * takes scr_arena_obj and is a block of its own. */ +struct ScrArena { + char *bump, *limit; + ScrArenaDisposal *disposals, *disposals_end; + uint32_t closed; + uint32_t open; /* still the allocation target, or under one that is */ + ScrArena *outer; /* the allocation target before it opened */ + ScrArena *next; /* its scope's list, or the pool */ + ScrArenaDisposal *disposal_base; + ScrArenaChunk *chunks; /* newest first */ + size_t bytes; +#if !SCR_SMALL_ALLOC + char **blocks; + size_t nblocks, cap; +#endif + _Alignas(16) char inline_chunk[SCR_ARENA_INLINE]; +}; +/* llvm/arenas.ts addresses these fields in pointer-sized units. */ +#define SCR_ARENA_PTR sizeof(void *) +_Static_assert(offsetof(ScrArena, bump) == 0 && offsetof(ScrArena, limit) == SCR_ARENA_PTR, + "llvm/arenas.ts bumps the arena inline"); +_Static_assert(offsetof(ScrArena, disposals) == 2 * SCR_ARENA_PTR && + offsetof(ScrArena, disposals_end) == 3 * SCR_ARENA_PTR && + offsetof(ScrArena, closed) == 4 * SCR_ARENA_PTR, + "llvm/arenas.ts appends to the dispose list inline"); +_Static_assert(sizeof(ScrArenaDisposal) == 2 * SCR_ARENA_PTR, + "llvm/arenas.ts stores dispose entries of two pointers"); +_Static_assert(offsetof(ScrArenaHead, disposals) == offsetof(ScrArena, disposals) && + offsetof(ScrArenaHead, closed) == offsetof(ScrArena, closed), + "ScrArenaHead is ScrArena's prefix"); + +SCR_TL ScrArena *scr_arena_cur = NULL; +static SCR_TL ScrArena *scr_arena_pool; +static SCR_TL unsigned scr_arena_pooled; +static SCR_TL ScrArenaChunk *scr_arena_chunk_pool; +static SCR_TL size_t scr_arena_chunk_bytes; + +void *scr_arena_current(void) { return scr_arena_cur; } + +void *scr_arena_suspend(void) { + ScrArena *a = scr_arena_cur; + scr_arena_cur = NULL; + return a; +} + +void scr_arena_resume(void *arena) { scr_arena_cur = (ScrArena *)arena; } + +void *scr_arena_open(void **list, int32_t closed) { + ScrArena *a = scr_arena_pool; + if (a) { + scr_arena_pool = a->next; + scr_arena_pooled--; + } else { + a = malloc(sizeof *a); + if (!a) scr_trap("scriptc: out of memory\n"); + a->disposal_base = NULL; + a->disposals_end = NULL; +#if !SCR_SMALL_ALLOC + a->blocks = NULL; + a->cap = 0; +#endif + } + a->closed = closed != 0; + a->chunks = NULL; + a->bytes = 0; + a->disposals = a->disposal_base; +#if SCR_SMALL_ALLOC + a->bump = a->inline_chunk; + a->limit = a->inline_chunk + SCR_ARENA_INLINE; +#else + a->bump = a->limit = NULL; + a->nblocks = 0; +#endif + a->open = 1; + a->outer = scr_arena_cur; + scr_arena_cur = a; + a->next = (ScrArena *)*list; + *list = a; + return a; +} + +/* Makes `a`'s outer target current again, if `a` is still the target or + * under it: an exception can leave the arenas a call opened unclosed until + * their scopes drop them. */ +static void scr_arena_unlink(ScrArena *a) { + if (!a->open) return; + a->open = 0; + for (ScrArena *t = scr_arena_cur; t; t = t->outer) + if (t == a) { + scr_arena_cur = a->outer; + return; + } +} + +void scr_arena_close(void *arena) { + if (arena) scr_arena_unlink((ScrArena *)arena); +} + +void scr_arena_disposal(void *arena, void *obj, ScrArenaDispose dispose) { + ScrArena *a = (ScrArena *)arena; + if (a->disposals == a->disposals_end) { + size_t n = (size_t)(a->disposals - a->disposal_base); + size_t cap = n ? n * 2 : 64; + ScrArenaDisposal *grown = realloc(a->disposal_base, cap * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + a->disposal_base = grown; + a->disposals = grown + n; + a->disposals_end = grown + cap; + } + a->disposals->obj = obj; + a->disposals->dispose = dispose; + a->disposals++; +} + +#if SCR_SMALL_ALLOC +static ScrArenaChunk *scr_arena_chunk(size_t want) { + for (ScrArenaChunk **p = &scr_arena_chunk_pool; *p; p = &(*p)->next) + if ((*p)->size >= want) { + ScrArenaChunk *c = *p; + *p = c->next; + scr_arena_chunk_bytes -= c->size; + return c; + } + ScrArenaChunk *c = malloc(want); + if (!c) scr_trap("scriptc: out of memory\n"); + c->size = want; + return c; +} + +static char *scr_arena_chunk_start(ScrArenaChunk *c) { + return (char *)(((uintptr_t)(c + 1) + 15) & ~(uintptr_t)15); +} + +/* A new current chunk with room for `n` bytes, or false past the cap + * when `capped`. */ +static bool scr_arena_refill(ScrArena *a, size_t n, bool capped) { + size_t want = a->chunks ? a->chunks->size * 2 : SCR_ARENA_CHUNK_MIN; + if (want > SCR_ARENA_CHUNK_MAX) want = SCR_ARENA_CHUNK_MAX; + size_t need = n + sizeof(ScrArenaChunk) + 15; + if (want < need) want = need; + if (capped && a->bytes + want > SCR_ARENA_CAP) return false; + ScrArenaChunk *c = scr_arena_chunk(want); + a->bytes += c->size; + c->next = a->chunks; + a->chunks = c; + a->bump = scr_arena_chunk_start(c); + a->limit = (char *)c + c->size; + return true; +} +#endif + +void *scr_arena_obj(void *arena, size_t size, void *cyc) { + ScrArena *a = (ScrArena *)arena; + size_t hdr = cyc ? sizeof(ScrCycHdr) : 0; + bool capped = !a->closed && !cyc; + if (capped && size > SCR_ARENA_CAP) return NULL; + size_t n = (hdr + size + 7) & ~(size_t)7; +#if SCR_SMALL_ALLOC + if ((size_t)(a->limit - a->bump) < n && !scr_arena_refill(a, n, capped)) return NULL; + char *block = a->bump; + a->bump += n; + memset(block, 0, n); +#else + if (capped && a->bytes + n > SCR_ARENA_CAP) return NULL; + a->bytes += n; + if (a->nblocks == a->cap) { + size_t cap = a->cap ? a->cap * 2 : 16; + char **grown = realloc(a->blocks, cap * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + a->blocks = grown; + a->cap = cap; + } + char *block = calloc(1, n); + if (!block) scr_trap("scriptc: out of memory\n"); + a->blocks[a->nblocks++] = block; +#endif + if (!cyc) return block; + ScrCycHdr *ch = (ScrCycHdr *)block; + ch->id = (uint16_t)scr_cyc_type_id((ScrCycType *)cyc); + ch->buf = SCR_CYC_ACYCLIC; + return block + hdr; +} + +static void scr_arena_free(ScrArena *a) { + /* Every entry before any memory: a release can reach another object of + * the arena. Newest first, the reverse of construction. */ + for (ScrArenaDisposal *d = a->disposals; d != a->disposal_base;) { + d--; + d->dispose(d->obj); + } + if ((size_t)(a->disposals_end - a->disposal_base) > SCR_ARENA_POOL_DISPOSALS) { + free(a->disposal_base); + a->disposal_base = a->disposals_end = NULL; + } +#if SCR_SMALL_ALLOC + for (ScrArenaChunk *c = a->chunks, *next; c; c = next) { + next = c->next; + if (scr_arena_chunk_bytes + c->size <= SCR_ARENA_POOL_BYTES) { + c->next = scr_arena_chunk_pool; + scr_arena_chunk_pool = c; + scr_arena_chunk_bytes += c->size; + } else + free(c); + } + if (scr_arena_pooled < SCR_ARENA_POOL) { + a->next = scr_arena_pool; + scr_arena_pool = a; + scr_arena_pooled++; + return; + } +#else + for (size_t i = 0; i < a->nblocks; i++) free(a->blocks[i]); + free(a->blocks); +#endif + free(a->disposal_base); + free(a); +} + +void scr_arena_drop(void **list) { + ScrArena *a = (ScrArena *)*list; + *list = NULL; + while (a) { + ScrArena *next = a->next; + scr_arena_unlink(a); + scr_arena_free(a); + a = next; + } +} diff --git a/packages/runtime/src/scr_arena.h b/packages/runtime/src/scr_arena.h new file mode 100644 index 000000000..cdfe5a476 --- /dev/null +++ b/packages/runtime/src/scr_arena.h @@ -0,0 +1,115 @@ +#ifndef SCR_ARENA_H +#define SCR_ARENA_H +/* Group arenas (llvm/region-groups.ts, llvm/arenas.ts): the objects a call + * allocates while the compiler has proven they die together, bump-allocated + * and released at once. + * + * Two kinds of call open one: + * - a fresh-result call (a root) whose value its caller keeps in one scope: + * the arena stays the allocation target while the call runs, and is + * dropped when that scope exits, on every path; + * - a call region, a call of a function that keeps nothing it allocates + * and returns no reference: the arena is dropped when the call returns. + * + * Only allocation sites in functions the compiler reached from such a call + * consult the target (scr_arena_cur), and only those whose value the group + * keeps (llvm/region-sites.ts), so runtime-internal allocations and + * temporaries never land in an arena. + * + * An arena object is an ordinary object with an ordinary count, starting + * at SCR_ARENA_RC instead of 1, so no release reaches zero while it lives: + * no teardown, no free, and (with `buf` SCR_CYC_ACYCLIC) never a collector + * candidate. The count is far below SCR_RC_PUBLISHED, so the arena object + * stays mutable and thread-local like any other. Dropping the arena runs + * its dispose list, whose entries release their objects' fields (and + * storage) without freeing the objects, and then recycles the memory. A + * closed arena's objects hold only each other and immortal values, so only + * objects with storage of their own (arrays) go on its list. + * + * Without the small-object allocator (sanitized and audit builds), every + * arena object is a block of its own that the drop frees, so a use after + * the drop is a use after free that the sanitizer reports. */ +#include "scr_core.h" + +#include +#include + +/* The starting count of an arena object (a count, in the low 32 bits of + * the rc word, so it also suits classes that keep their id in the high + * half). */ +#define SCR_ARENA_RC ((uint32_t)1 << 29) + +typedef struct ScrArena ScrArena; +/* Releases what an arena object holds (its fields, an array's storage) + * without freeing the object. */ +typedef void (*ScrArenaDispose)(void *obj); + +/* An entry of an arena's dispose list. The emitted arena-aware + * constructors append one inline for an object whose type has fields to + * release, unless the arena is closed (llvm/arenas.ts). */ +typedef struct ScrArenaDisposal { + void *obj; + ScrArenaDispose dispose; +} ScrArenaDisposal; + +/* The arena that allocation sites in arena-aware functions use, or NULL. + * Emitted code reads scr_arena_cur directly outside worker programs. */ +extern SCR_TL ScrArena *scr_arena_cur; +void *scr_arena_current(void); +/* Stops any arena being the allocation target while a call whose value + * its group does not keep runs; returns the target, which + * scr_arena_resume restores. */ +void *scr_arena_suspend(void); +void scr_arena_resume(void *arena); +/* Opens an arena as the allocation target and links it onto `*list`, the + * list its scope drops. Returns it. A `closed` arena's objects hold only + * each other and immortal values (the compiler proved it), so dropping it + * releases nothing. */ +void *scr_arena_open(void **list, int32_t closed); +/* Stops `arena` (may be NULL) being the allocation target; its objects + * live on until its list is dropped. */ +void scr_arena_close(void *arena); +/* Drops every arena on `*list`, newest first, and clears the list. */ +void scr_arena_drop(void **list); +/* A zeroed object of `size` bytes in `arena` (behind a collector header + * when `cyc` is not NULL, its `buf` SCR_CYC_ACYCLIC), for when the emitted + * bump finds no room: a new chunk. Returns NULL when an arena that is not + * closed is past its byte cap and `cyc` is NULL, and the caller allocates + * on the heap. The caller stores the count (SCR_ARENA_RC). */ +void *scr_arena_obj(void *arena, size_t size, void *cyc); +/* Appends `obj` to `arena`'s dispose list, which the drop runs. */ +void scr_arena_disposal(void *arena, void *obj, ScrArenaDispose dispose); + +/* The leading fields of an arena (scr_arena.c's ScrArena), for the + * runtime's own inline allocation. */ +typedef struct ScrArenaHead { + char *bump, *limit; + ScrArenaDisposal *disposals, *disposals_end; + uint32_t closed; +} ScrArenaHead; + +/* `n` bytes (a multiple of 8) from `arena`'s current chunk, not cleared, + * or NULL when the chunk has no room (scr_arena_obj takes a new one). */ +static inline void *scr_arena_bump(void *arena, size_t n) { + ScrArenaHead *h = (ScrArenaHead *)arena; + if ((size_t)(h->limit - h->bump) < n) return NULL; + char *p = h->bump; + h->bump = p + n; + return p; +} + +/* scr_arena_disposal with the append inline. */ +static inline void scr_arena_dispose_later(void *arena, void *obj, ScrArenaDispose dispose) { + ScrArenaHead *h = (ScrArenaHead *)arena; + if (h->disposals == h->disposals_end) { + scr_arena_disposal(arena, obj, dispose); + return; + } + h->disposals->obj = obj; + h->disposals->dispose = dispose; + h->disposals++; +} + +static inline int scr_arena_is_closed(void *arena) { return ((ScrArenaHead *)arena)->closed != 0; } + +#endif diff --git a/packages/runtime/src/scr_arena.test.c b/packages/runtime/src/scr_arena.test.c new file mode 100644 index 000000000..8f369d4e3 --- /dev/null +++ b/packages/runtime/src/scr_arena.test.c @@ -0,0 +1,108 @@ +/* Group arenas (scr_arena.h): allocation, nesting, closing and dropping, + * and the drop's dispose list. Prints one line per check. */ +#include "scr_runtime.h" + +#include +#include + +typedef struct { + int64_t rc; + int64_t id; +} TestObj; + +static int disposed; +static int64_t disposed_sum; +static void test_dispose(void *obj) { + disposed++; + disposed_sum += ((TestObj *)obj)->id; +} +/* What an emitted arena-aware constructor does: the object, and its + * dispose entry unless the arena is closed. */ +static TestObj *obj(void *arena, int64_t id, int closed) { + TestObj *o = scr_arena_obj(arena, sizeof *o, NULL); + if (!o) return NULL; + o->rc = SCR_ARENA_RC; + o->id = id; + if (!closed) scr_arena_disposal(arena, o, test_dispose); + return o; +} + +int main(void) { + /* An open arena is the allocation target until it closes. */ + void *list = NULL; + void *a = scr_arena_open(&list, 0); + printf("current while open: %s\n", scr_arena_current() == a ? "yes" : "no"); + TestObj *first = obj(a, 1, 0); + int zero = 1, adjacent = 1; + TestObj *prev = first; + for (int i = 0; i < 16; i++) { + TestObj *o = scr_arena_obj(a, sizeof *o, NULL); + if (o->rc != 0 || o->id != 0) zero = 0; + if ((char *)o != (char *)(prev + 1)) adjacent = 0; + prev = o; + } + printf("objects start zeroed: %s\n", zero ? "yes" : "no"); +#if !SCR_SMALL_ALLOC + adjacent = 1; /* a block per object */ +#endif + printf("objects without headers: %s\n", adjacent ? "yes" : "no"); + /* A nested arena makes the outer one current again when it closes. */ + void *inner_list = NULL; + void *b = scr_arena_open(&inner_list, 0); + for (int i = 0; i < 10; i++) obj(b, 100, 0); + scr_arena_close(b); + scr_arena_close(b); + printf("outer current after inner close: %s\n", scr_arena_current() == a ? "yes" : "no"); + scr_arena_drop(&inner_list); + printf("inner drop disposed %d (sum %lld)\n", disposed, (long long)disposed_sum); + /* Many objects span chunks; each is disposed once. */ + disposed = 0; + disposed_sum = 0; + for (int i = 0; i < 100000; i++) obj(a, 1, 0); + scr_arena_close(a); + printf("current after close: %s\n", scr_arena_current() == NULL ? "none" : "some"); + scr_arena_drop(&list); + printf("drop disposed %d (sum %lld), list %s\n", disposed, (long long)disposed_sum, + list == NULL ? "cleared" : "kept"); + + /* A closed arena's objects take no dispose entries, and it has no cap. */ + disposed = 0; + void *closed = scr_arena_open(&list, 1); + int held = 0; + for (int i = 0; i < 3000000; i++) + if (obj(closed, 1, 1)) held++; + scr_arena_drop(&list); + printf("closed arena held %d, disposed %d, current %s\n", held, disposed, + scr_arena_current() == NULL ? "none" : "some"); + + /* Past the byte cap an arena that is not closed refuses, and its drop + * disposes everything it holds. */ + disposed = 0; + void *capped = scr_arena_open(&list, 0); + int refused = 0; + held = 0; + for (int i = 0; i < 3000000 && !refused; i++) { + if (obj(capped, 1, 0)) + held++; + else + refused = 1; + } + scr_arena_drop(&list); + printf("cap refused: %s, all disposed: %s\n", refused ? "yes" : "no", disposed == held ? "yes" : "no"); + + /* A drop of an arena that is still open (an exception skipped its close) + * restores the target it replaced. */ + void *outer_list = NULL, *skipped_list = NULL; + void *outer = scr_arena_open(&outer_list, 0); + scr_arena_open(&skipped_list, 0); + scr_arena_drop(&skipped_list); + printf("target after dropping an open arena: %s\n", scr_arena_current() == outer ? "outer" : "other"); + /* A suspended arena is no target until it resumes. */ + void *saved = scr_arena_suspend(); + printf("target while suspended: %s\n", scr_arena_current() == NULL ? "none" : "some"); + scr_arena_resume(saved); + printf("target after resume: %s\n", scr_arena_current() == outer ? "outer" : "other"); + scr_arena_close(outer); + scr_arena_drop(&outer_list); + return 0; +} diff --git a/packages/runtime/src/scr_arena.test.ts b/packages/runtime/src/scr_arena.test.ts new file mode 100644 index 000000000..0e48c8ea8 --- /dev/null +++ b/packages/runtime/src/scr_arena.test.ts @@ -0,0 +1,67 @@ +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 { describe, expect, test } from "vitest"; +import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js"; + +const exec = promisify(execFile); + +const EXPECTED = [ + "current while open: yes", + "objects start zeroed: yes", + "objects without headers: yes", + "outer current after inner close: yes", + "inner drop disposed 10 (sum 1000)", + "current after close: none", + "drop disposed 100001 (sum 100001), list cleared", + "closed arena held 3000000, disposed 0, current none", + "cap refused: yes, all disposed: yes", + "target after dropping an open arena: outer", + "target while suspended: none", + "target after resume: outer", +]; + +// The size-class allocator build (chunks, the inline bump's fields), and +// the sanitized lane's build (a block per object, freed by the drop). +describe.skipIf(process.platform === "win32")("group arenas", () => { + test.each([ + ["size-class", ["-fsanitize=undefined"]], + ["sanitized", ["-fsanitize=address,undefined", "-DSCR_RC_AUDIT"]], + ])( + "open, allocate, close and drop (%s)", + async (_lane, flags) => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-arena-")); + try { + const bin = join(dir, "arena"); + await exec("clang", [ + "-std=c11", + "-O1", + "-pthread", + "-Wno-deprecated-declarations", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + ...flags, + ...["scr_arena.test.c", ...EXECUTABLE_RUNTIME_SOURCES].map((source) => + join(import.meta.dirname, source), + ), + ...(process.platform === "linux" ? ["-lm"] : []), + "-o", + bin, + ]); + const { stdout } = await exec(bin, [], { + timeout: 60000, + env: { + ...process.env, + ASAN_OPTIONS: "halt_on_error=1", + UBSAN_OPTIONS: "halt_on_error=1", + }, + }); + expect(stdout.trim().split("\n")).toEqual(EXPECTED); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 300000, + ); +}); diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 9d4884390..0e4513c6f 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -1426,15 +1426,53 @@ static void scr_arr_put_slots(ScrArr *a, size_t to, size_t count, const uint64_t /* Inlined into each constructor, so `small` and the slot unit fold to * constants there. */ +static void scr_arr_arena_dispose(void *object); +static void scr_arr_arena_dispose_closed(void *object); +static ScrCycType scr_arr_cyc_type; + +/* An array block of `size` bytes in the current group arena, or NULL. + * Its storage beyond the block is the heap's, so it goes on the arena's + * dispose list even when the arena is closed; there, its elements are + * arena objects and immortal values, which need no release. */ +static inline ScrArr *scr_arr_arena_block(size_t size, bool cyc) { + void *arena = scr_arena_cur; + if (!arena) return NULL; + size_t hdr = cyc ? sizeof(ScrCycHdr) : 0; + size_t n = (hdr + size + 7) & ~(size_t)7; + uint32_t id = cyc ? scr_cyc_type_id(&scr_arr_cyc_type) : 0; + char *block = scr_arena_bump(arena, n); + ScrArr *a; + if (block) { + /* The constructor sets every header field, as on the heap; only the + * collector header needs setting here. */ + if (cyc) *(ScrCycHdr *)block = (ScrCycHdr){.id = (uint16_t)id, .buf = SCR_CYC_ACYCLIC}; + a = (ScrArr *)(block + hdr); + } else { + a = scr_arena_obj(arena, size, cyc ? &scr_arr_cyc_type : NULL); + if (!a) return NULL; + } + scr_arena_dispose_later(arena, a, + scr_arena_is_closed(arena) ? scr_arr_arena_dispose_closed + : scr_arr_arena_dispose); + return a; +} + static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind elem, size_t initial_cap, - bool small) { + bool small, + bool arena) { size_t unit = scr_elem_slot_width(elem); size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, unit) : scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, unit); - ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); - if (!a) scr_arr_oom(); - a->rc = 1; + size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); + ScrArr *a = arena ? scr_arr_arena_block(size, false) : NULL; + if (a) + a->rc = SCR_ARENA_RC; + else { + a = scr_mem_alloc(size); + if (!a) scr_arr_oom(); + a->rc = 1; + } a->len = 0; a->cap = 0; a->elem = (uint8_t)elem; @@ -1451,10 +1489,16 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el } ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap) { - return scr_arr_make(elem, initial_cap, false); + return scr_arr_make(elem, initial_cap, false, false); } -ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true); } +ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true, false); } + +ScrArr *scr_arr_new_r(ScrElemKind elem, size_t initial_cap) { + return scr_arr_make(elem, initial_cap, false, true); +} + +ScrArr *scr_arr_new_small_r(ScrElemKind elem) { return scr_arr_make(elem, 0, true, true); } /* A non-cycle array whose final length the runtime knows when it builds * it (regex match results): its `cap` slots share the header's block, so @@ -1463,7 +1507,7 @@ ScrArr *scr_arr_new_small(ScrElemKind elem) { return scr_arr_make(elem, 0, true) * `cap` moves the slots out like any inline array. */ ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states) { if (cap == 0) { - ScrArr *a = scr_arr_make(elem, 0, false); + ScrArr *a = scr_arr_make(elem, 0, false, false); if (states) a->spare |= SCR_ARR_STATES; return a; } @@ -1572,13 +1616,16 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } +static ScrCycType scr_arr_cyc_type = {&scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY}; + /* Inlined into each constructor like scr_arr_make: with `small`, the count * and `elem` constant, scr_arr_new_ops_small's size class folds to a table * index and the slot sizes to constants. */ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const ScrArrOps *ops, size_t initial_cap, bool small, - uint8_t elem) { + uint8_t elem, + bool arena) { ScrArr *a; bool cyc = ops->trace != NULL; /* The array starts without states: slots of the value width alone. */ @@ -1587,14 +1634,18 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const size_t inline_cap = small ? scr_arr_small_cap(prefix, unit) : scr_arr_known_cap(prefix, initial_cap, unit); size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr); - if (cyc) { - static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; - a = scr_cyc_alloc_inline(size, &cyc_type); - } else { - a = scr_mem_alloc(size); - if (!a) scr_arr_oom(); + a = arena ? scr_arr_arena_block(size, cyc) : NULL; + if (a) + a->rc = SCR_ARENA_RC; + else { + if (cyc) + a = scr_cyc_alloc_inline(size, &scr_arr_cyc_type); + else { + a = scr_mem_alloc(size); + if (!a) scr_arr_oom(); + } + a->rc = 1; } - a->rc = 1; a->len = 0; a->cap = 0; a->elem = elem; @@ -1622,17 +1673,26 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops_kind(const * branch inlines scr_arr_make_ops_kind with a constant kind. */ static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, - bool small) { + bool small, + bool arena) { if (__builtin_expect(scr_arr_ops_compact(ops), 0)) - return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF); - return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_CREF, arena); + return scr_arr_make_ops_kind(ops, initial_cap, small, SCR_ELEM_REF, arena); } ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap) { - return scr_arr_make_ops(ops, initial_cap, false); + return scr_arr_make_ops(ops, initial_cap, false, false); } -ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true); } +ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true, false); } + +/* The constructors arena-aware functions call (llvm/arenas.ts): in the + * current group arena when one is open. */ +ScrArr *scr_arr_new_ops_r(const ScrArrOps *ops, size_t initial_cap) { + return scr_arr_make_ops(ops, initial_cap, false, true); +} + +ScrArr *scr_arr_new_ops_small_r(const ScrArrOps *ops) { return scr_arr_make_ops(ops, 0, true, true); } /* Frees an acyclic array whose elements are already released. */ static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) { @@ -1669,8 +1729,8 @@ static void scr_arr_release_foreign(ScrArr *a) { x->foreign = false; } -static void scr_arr_destroy(void *object) { - ScrArr *a = object; +/* 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; @@ -1737,10 +1797,275 @@ 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); } +/* An arena array at its arena's drop: its contents and the storage beyond + * its block go; the block goes with the arena. */ +static void scr_arr_arena_dispose(void *object) { + ScrArr *a = object; + scr_arr_release_contents(a); + scr_arr_free_parts(a); +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif +} + +/* The same in a closed arena, whose objects hold only each other and + * immortal values: only the storage beyond the block goes. */ +static void scr_arr_arena_dispose_closed(void *object) { + scr_arr_free_parts((ScrArr *)object); +#ifdef SCR_RC_AUDIT + scr_live_arrays--; +#endif +} + +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). */ @@ -1754,6 +2079,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)) { @@ -5711,17 +6042,39 @@ 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 < scr_dyn_obj_count(fields); i++) + scr_dyn_copy_prop_at(target, fields, 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)) return; metadata = SCR_AX(a)->metadata; if (!metadata) return; - for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) scr_dyn_copy_prop_at(target, metadata, i); } ScrArr *scr_arr_named_keys(ScrArr *a) { @@ -5733,6 +6086,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 < scr_dyn_obj_count(fields); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(fields, 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"}; @@ -5740,10 +6102,10 @@ ScrArr *scr_arr_named_keys(ScrArr *a) { return keys; } if (!metadata || metadata->kind != SCR_DYN_OBJ) return keys; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - if (!entry->enumerable || scr_arr_prop_index(a, entry->key, entry->key_len) != SIZE_MAX) continue; - scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len)); + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(metadata, i); + if (!entry.enumerable || scr_arr_prop_index(a, entry.key, entry.key_len) != SIZE_MAX) continue; + scr_arr_push_ref(keys, scr_str_new(entry.key, entry.key_len)); } return keys; } @@ -5756,15 +6118,22 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key) { if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) return true; /* index, input and groups, the only keys a pending result lists */ if (!metadata || metadata->kind != SCR_DYN_OBJ) return false; - for (size_t i = 0; i < metadata->v.obj.len; i++) { - const ScrDynEntry *entry = &metadata->v.obj.entries[i]; - if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return true; + for (size_t i = 0; i < scr_dyn_obj_count(metadata); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(metadata, i); + if (entry.key_len == key->len && memcmp(entry.key, key->data, key->len) == 0) return true; } return false; } 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); @@ -5778,20 +6147,22 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { from->prop_cap = cap; } ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL; - if (!named || named->v.obj.len == 0) return; + if (!named || scr_dyn_obj_count(named) == 0) return; /* The view's numeric properties are copies of the native ones (every * write to them goes to the native array), so only the others move. */ bool numeric = false; - for (size_t i = 0; i < named->v.obj.len && !numeric; i++) { + for (size_t i = 0; i < scr_dyn_obj_count(named) && !numeric; i++) { double number; - numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number); + size_t key_len; + const char *key = scr_dyn_obj_key_at(named, i, &key_len); + numeric = scr_arr_numeric_key(key, key_len, &number); } if (numeric) { ScrDyn *rest = scr_dyn_new_obj(); - for (size_t i = 0; i < named->v.obj.len; i++) { - const ScrDynEntry *entry = &named->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(named); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(named, i); double number; - if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry); + if (!scr_arr_numeric_key(entry.key, entry.key_len, &number)) scr_dyn_copy_prop_at(rest, named, i); } named = rest; } else { @@ -5799,7 +6170,7 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { } ScrArrExt *x = scr_arr_ext(a); ScrDyn *old = x->metadata; - x->metadata = named->v.obj.len ? named : NULL; + x->metadata = scr_dyn_obj_count(named) ? named : NULL; if (!x->metadata) scr_dyn_release(named); scr_dyn_release(old); } diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index fab85fff1..86ef8d999 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -175,10 +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 @@ -466,6 +490,12 @@ ScrArr *scr_arr_new_small(ScrElemKind elem); * SCR_ELEM_F64); no-op for any other kind. */ void scr_arr_widen_i32(ScrArr *a); ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); +/* The same constructors in the current group arena, when one is open + * (scr_arena.h): what arena-aware functions call. */ +ScrArr *scr_arr_new_r(ScrElemKind elem, size_t initial_cap); +ScrArr *scr_arr_new_small_r(ScrElemKind elem); +ScrArr *scr_arr_new_ops_r(const ScrArrOps *ops, size_t initial_cap); +ScrArr *scr_arr_new_ops_small_r(const ScrArrOps *ops); /* scr_arr_new(elem, cap) for a known final length: the `cap` slots share * the array's own block (one allocation). With `states`, the array starts * with states (for holes or present undefined). Returns +1. */ @@ -482,11 +512,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 diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index bf5d1c875..6e9384f15 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -718,7 +718,7 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) { if (a->v.arr.properties || b->v.arr.properties) { if (!a->v.arr.properties || !b->v.arr.properties) { const ScrDyn *table = a->v.arr.properties ? a->v.arr.properties : b->v.arr.properties; - if (table->v.obj.len != 0) return false; + if (scr_dyn_obj_count(table) != 0) return false; } else if (!scr_assert_dyn_deep_eq(a->v.arr.properties, b->v.arr.properties)) return false; } return true; @@ -738,11 +738,11 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) { } else if (a->null_proto != b->null_proto || a->prototype != b->prototype) { return false; } - if (a->v.obj.len != b->v.obj.len) return false; - for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *ent = &a->v.obj.entries[i]; - ScrDyn *bv = scr_dyn_obj_get(b, ent->key, ent->key_len); - if (!bv || !scr_assert_dyn_deep_eq(ent->value, bv)) return false; + if (scr_dyn_obj_count(a) != scr_dyn_obj_count(b)) return false; + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(a, i); + ScrDyn *bv = scr_dyn_obj_get(b, ent.key, ent.key_len); + if (!bv || !scr_assert_dyn_deep_eq(ent.value, bv)) return false; } return true; } @@ -782,30 +782,29 @@ static bool scr_assert_loose_nan(const ScrDyn *d) { return d->kind == SCR_DYN_NUM && d->v.num != d->v.num; } -/* An enumerable own entry, or NULL. Borrowed. */ -static const ScrDynEntry *scr_assert_loose_entry(const ScrDyn *d, const char *key, size_t len) { - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; - if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? ent : NULL; - } - return NULL; +/* The value of an enumerable own property, or NULL. Borrowed. */ +static const ScrDyn *scr_assert_loose_entry(const ScrDyn *d, const char *key, size_t len) { + size_t i = scr_dyn_obj_index_of(d, key, len); + if (i == SIZE_MAX) return NULL; + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + return ent.enumerable ? ent.value : NULL; } -static bool scr_assert_loose_counted(const ScrDynEntry *ent) { - return ent->enumerable && !(ent->key_len == 6 && memcmp(ent->key, "%error", 6) == 0); +static bool scr_assert_loose_counted(ScrDynProp ent) { + return ent.enumerable && !(ent.key_len == 6 && memcmp(ent.key, "%error", 6) == 0); } /* keyCheck and objEquiv over two objects' enumerable own keys. */ static bool scr_assert_loose_keys(const ScrDyn *a, const ScrDyn *b) { size_t na = 0, nb = 0; - for (size_t i = 0; i < a->v.obj.len; i++) na += scr_assert_loose_counted(&a->v.obj.entries[i]); - for (size_t i = 0; i < b->v.obj.len; i++) nb += scr_assert_loose_counted(&b->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) na += scr_assert_loose_counted(scr_dyn_obj_prop_at(a, i)); + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) nb += scr_assert_loose_counted(scr_dyn_obj_prop_at(b, i)); if (na != nb) return false; - for (size_t i = 0; i < b->v.obj.len; i++) { - const ScrDynEntry *ent = &b->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(b, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = scr_assert_loose_entry(a, ent->key, ent->key_len); - if (!other || !scr_assert_loose_eq(other->value, ent->value)) return false; + const ScrDyn *other = scr_assert_loose_entry(a, ent.key, ent.key_len); + if (!other || !scr_assert_loose_eq(other, ent.value)) return false; } return true; } @@ -1180,8 +1179,8 @@ static bool scr_assert_loose_array(const ScrDyn *a, const ScrDyn *b) { if (pa || pb) { if (!pa || !pb) { const ScrDyn *table = pa ? pa : pb; - for (size_t i = 0; i < table->v.obj.len; i++) - if (scr_assert_loose_counted(&table->v.obj.entries[i])) return false; + for (size_t i = 0; i < scr_dyn_obj_count(table); i++) + if (scr_assert_loose_counted(scr_dyn_obj_prop_at(table, i))) return false; } else if (!scr_assert_loose_keys(pa, pb)) { return false; } @@ -1295,7 +1294,7 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent, * or `{`, then its entries. */ static void scr_assert_cf_object(ScrAssertBuf *b, const ScrDyn *d, size_t indent, size_t depth, const char *cls) { - if (d->v.obj.len == 0) { + if (scr_dyn_obj_count(d) == 0) { if (cls) { ab_cstr(b, cls); ab_cstr(b, " {}"); @@ -1320,25 +1319,25 @@ static void scr_assert_cf_object(ScrAssertBuf *b, const ScrDyn *d, size_t indent } else if (d->null_proto) ab_cstr(b, "[Object: null prototype] "); /* Render every entry, then sort the RENDERED texts (Node's * sorted:true sorts formatted entries, quotes included). */ - ScrCfEntry *ents = malloc(d->v.obj.len * sizeof *ents); + ScrCfEntry *ents = malloc(scr_dyn_obj_count(d) * sizeof *ents); if (!ents) scr_assert_oom(); - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); ScrAssertBuf eb = {0}; - scr_assert_cf_key(&eb, ent->key, ent->key_len); + scr_assert_cf_key(&eb, ent.key, ent.key_len); ab_cstr(&eb, ": "); - scr_assert_cf_value(&eb, ent->value, indent + 2, depth - 1); + scr_assert_cf_value(&eb, ent.value, indent + 2, depth - 1); ents[i].text = eb.data ? eb.data : malloc(1); if (!ents[i].text) scr_assert_oom(); ents[i].len = eb.len; } - qsort(ents, d->v.obj.len, sizeof *ents, scr_assert_cf_entry_cmp); + qsort(ents, scr_dyn_obj_count(d), sizeof *ents, scr_assert_cf_entry_cmp); ab_char(b, '{'); - for (size_t i = 0; i < d->v.obj.len; i++) { + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { ab_char(b, '\n'); scr_assert_cf_pad(b, indent + 2); ab_bytes(b, ents[i].text, ents[i].len); - if (i + 1 < d->v.obj.len) ab_char(b, ','); + if (i + 1 < scr_dyn_obj_count(d)) ab_char(b, ','); free(ents[i].text); } free(ents); @@ -1364,11 +1363,11 @@ static const char *scr_assert_cf_bytes_class(ScrBytesElem elem) { } -static bool scr_assert_cf_error_extra(const ScrDynEntry *ent) { - if (!ent->enumerable || (ent->key_len > 0 && ent->key[0] == '%')) return false; - return !((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)); +static bool scr_assert_cf_error_extra(ScrDynProp ent) { + if (!ent.enumerable || (ent.key_len > 0 && ent.key[0] == '%')) return false; + return !((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)); } /* An encoded Error: Node's stackless `[Name: message]` (assertion_error.js @@ -1386,8 +1385,8 @@ static void scr_assert_cf_error(ScrAssertBuf *b, const ScrDyn *d, size_t indent, } ab_char(b, ']'); size_t extra = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); if (scr_assert_cf_error_extra(ent)) extra++; } if (extra == 0) return; @@ -1395,13 +1394,13 @@ static void scr_assert_cf_error(ScrAssertBuf *b, const ScrDyn *d, size_t indent, ScrCfEntry *ents = malloc(extra * sizeof *ents); if (!ents) scr_assert_oom(); size_t n = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); if (!scr_assert_cf_error_extra(ent)) continue; ScrAssertBuf eb = {0}; - scr_assert_cf_key(&eb, ent->key, ent->key_len); + scr_assert_cf_key(&eb, ent.key, ent.key_len); ab_cstr(&eb, ": "); - scr_assert_cf_value(&eb, ent->value, indent + 2, depth - 1); + scr_assert_cf_value(&eb, ent.value, indent + 2, depth - 1); ents[n].text = eb.data ? eb.data : malloc(1); if (!ents[n].text) scr_assert_oom(); ents[n++].len = eb.len; @@ -1989,11 +1988,11 @@ static bool scr_assert_partial_eq(const ScrDyn *a, const ScrDyn *b); /* objEquiv over the expected side's enumerable own keys. */ static bool scr_assert_partial_keys(const ScrDyn *a, const ScrDyn *b) { - for (size_t i = 0; i < b->v.obj.len; i++) { - const ScrDynEntry *ent = &b->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(b); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(b, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = scr_assert_loose_entry(a, ent->key, ent->key_len); - if (!other || !scr_assert_partial_eq(other->value, ent->value)) return false; + const ScrDyn *other = scr_assert_loose_entry(a, ent.key, ent.key_len); + if (!other || !scr_assert_partial_eq(other, ent.value)) return false; } return true; } @@ -2043,11 +2042,11 @@ static bool scr_assert_partial_array(const ScrDyn *a, const ScrDyn *b) { const ScrDyn *pb = b->v.arr.properties; if (pb) { const ScrDyn *pa = a->v.arr.properties; - for (size_t i = 0; i < pb->v.obj.len; i++) { - const ScrDynEntry *ent = &pb->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(pb); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(pb, i); if (!scr_assert_loose_counted(ent)) continue; - const ScrDynEntry *other = pa ? scr_assert_loose_entry(pa, ent->key, ent->key_len) : NULL; - if (!other || !scr_assert_partial_eq(other->value, ent->value)) return false; + const ScrDyn *other = pa ? scr_assert_loose_entry(pa, ent.key, ent.key_len) : NULL; + if (!other || !scr_assert_partial_eq(other, ent.value)) return false; } } size_t pos = 0; @@ -2434,11 +2433,11 @@ void scr_assert_loose_deep_result(bool equal, bool negated, ScrStr *msg, bool ha void scr_assert_expects_err_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *msg, bool has_msg) { if (actual->kind == SCR_DYN_OBJ && expected->kind == SCR_DYN_OBJ) { bool ok = true; - for (size_t i = 0; i < expected->v.obj.len && ok; i++) { - const ScrDynEntry *e = &expected->v.obj.entries[i]; - if (e->key_len == 6 && memcmp(e->key, "%error", 6) == 0) continue; - ScrDyn *av = scr_dyn_obj_get(actual, e->key, e->key_len); - if (av == NULL || !scr_assert_dyn_deep_eq(av, e->value)) ok = false; + for (size_t i = 0; i < scr_dyn_obj_count(expected) && ok; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(expected, i); + if (e.key_len == 6 && memcmp(e.key, "%error", 6) == 0) continue; + ScrDyn *av = scr_dyn_obj_get(actual, e.key, e.key_len); + if (av == NULL || !scr_assert_dyn_deep_eq(av, e.value)) ok = false; } if (ok) return; } diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index f9c6944c2..fc06a9f06 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1518,7 +1518,11 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) { (void)to; scr_trap("scriptc: internal error: native stack switch reached on WASI\n"); #elif defined(SCR_ASAN_FIBERS) - void **save = from_fiber ? &from_fiber->fake_stack : &scr_main_fake_stack; + /* A finished fiber leaves for the last time: a NULL slot has + * AddressSanitizer free its fake stack, which otherwise stays mapped + * until exit. */ + void **save = from_fiber ? (from_fiber->done ? NULL : &from_fiber->fake_stack) + : &scr_main_fake_stack; const void *bottom = to_fiber ? to_fiber->stack : NULL; size_t size = to_fiber ? SCR_FIBER_STACK : 0; __sanitizer_start_switch_fiber(save, bottom, size); @@ -1527,10 +1531,13 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) { #else swapcontext(from, to); #endif + /* Back in this frame: the context that saved into `save` above runs + * again, whichever fiber `scr_current` names (a finished fiber returns + * to its resumer with no fiber to switch to). */ const void *old_bottom; size_t old_size; - __sanitizer_finish_switch_fiber( - scr_current ? scr_current->fake_stack : scr_main_fake_stack, &old_bottom, &old_size); + __sanitizer_finish_switch_fiber(from_fiber ? from_fiber->fake_stack : scr_main_fake_stack, + &old_bottom, &old_size); #elif defined(SCR_FAST_CTX) scr_ctx_swap(from, to); #else @@ -2228,6 +2235,7 @@ static void scr_prefix_trampoline(void) { scr_als_active = &f->als; entry(f, argpack); scr_current = carrier; + carrier->done = true; /* its stack is freed once the spawner is back */ scr_switch(&carrier->ctx, carrier->return_to, NULL); /* unreachable */ } @@ -5385,6 +5393,11 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa bool stackless) { ScrGen *g = scr_mem_calloc(sizeof *g); if (!g) scr_oom(); + /* Install the tag reader with each generator rather than in a constructor: + * a constructor is a link root, so it would keep scr_gen_tag and this + * unit's string literals in every executable, generators or not. Threads + * store the same value, relaxed. */ + __atomic_store_n(&scr_gen_tag_reader, scr_gen_tag, __ATOMIC_RELAXED); g->rc = 1; g->receiver = scr_dyn_this_get(); g->state = SCR_GEN_UNSTARTED; @@ -5435,6 +5448,14 @@ ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, return scr_gen_new_common(entry, argpack, drop_args, settle, false); } +#ifndef __wasi__ +ScrGen *scr_async_gen_new_stackless(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *), + void (*settle)(ScrGen *, ScrPromise *)) { + return scr_gen_new_common(entry, argpack, drop_args, settle, true); +} +#endif + ScrGen *scr_gen_retain(ScrGen *g) { if (g) g->rc++; return g; @@ -5783,7 +5804,7 @@ void scr_gen_ret_to_out(ScrGen *g) { * settle the generator's state, moving a finished body's exception to the * resumer and tearing the finished fiber down. `release` drops the hop's * own reference (see scr_gen_switch_in); `sync` marks a body known to be a - * synchronous generator (every stackless one is). */ + * synchronous generator. */ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen *g, ScrFiber *f, bool release, bool sync) { if (__builtin_expect(f->done, 0)) { @@ -5820,10 +5841,12 @@ static __attribute__((noinline)) void scr_async_gen_step_end(ScrFiber *f) { scr_ #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` - * holds the generator across the hop; a resumer whose own reference - * provably outlives the hop passes false. */ -__attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain) { + * until its next yield, an async generator's await, or its final + * suspension returns here. `retain` holds the generator across the hop; a + * resumer whose own reference provably outlives the hop passes false. + * `sync` marks a body known to be a synchronous generator. */ +__attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain, + bool sync) { ScrFiber *f = g->fiber; if (retain) scr_gen_retain(g); /* A synchronous generator has no AsyncLocalStorage context of its own: @@ -5848,10 +5871,29 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo 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); + scr_gen_switched_back(g, f, retain, sync); } -static void scr_gen_coro_in(ScrGen *g) { scr_gen_coro_hop(g, true); } +/* An async generator's step can resume the next one down a chain of them + * before it suspends, all on one stack, through runtime frames that check + * no guard: a step that would start below the stack guard throws the + * RangeError compiled code throws there, which rejects its request. */ +static __attribute__((noinline)) void scr_async_gen_coro_in(ScrGen *g) { + uintptr_t guard = scr_stack_guard; + if (guard != 0 && (uintptr_t)__builtin_frame_address(0) < guard) { + /* scr_stack_overflow's error, thrown here: this unit links without + * scr_context.c in the runtime's own tests. */ + static const char text[] = "Maximum call stack size exceeded"; + scr_throw_error_msg(SCR_ERR_RANGE, text, sizeof text - 1); + return; + } + scr_gen_coro_hop(g, true, false); +} + +static void scr_gen_coro_in(ScrGen *g) { + if (g->is_async) scr_async_gen_coro_in(g); + else scr_gen_coro_hop(g, true, true); +} #endif /* The consumer→fiber hop shared by every resume mode: switches in, and on @@ -5930,10 +5972,6 @@ const char *scr_gen_tag(const ScrGen *g) { return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator"; } -__attribute__((constructor)) static void scr_gen_tag_reader_init(void) { - scr_gen_tag_reader = scr_gen_tag; -} - void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); @@ -5947,8 +5985,9 @@ void scr_gen_close(ScrGen *g) { bool scr_gen_step(ScrGen *g) { if (g->in.kind != SCR_EXC_NONE) scr_gen_slot_reset(&g->in); #ifndef __wasi__ - if ((g->state == SCR_GEN_SUSPENDED || g->state == SCR_GEN_UNSTARTED) && g->fiber->stackless) { - scr_gen_coro_hop(g, false); + if ((g->state == SCR_GEN_SUSPENDED || g->state == SCR_GEN_UNSTARTED) && g->fiber->stackless && + !g->is_async) { + scr_gen_coro_hop(g, false, true); return g->state == SCR_GEN_DONE; } #endif diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index e31ba5b1d..d2e16be43 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -143,6 +143,9 @@ void scr_promise_rethrow_top_level(ScrPromise *p); * value), or NULL with the rejection re-thrown into the awaiter. */ ScrDyn *scr_await_dyn(ScrPromise *p); ScrDyn *scr_await_dyn_value_settled(ScrDyn *value); +/* The native coroutine form's extraction: the body has already suspended + * until the value's promise settled, or taken the non-promise hop. */ +ScrDyn *scr_coro_await_dyn_value(ScrDyn *value); void scr_promise_resolve_dyn(ScrPromise *destination, ScrDyn *value); /* `await v` over a checked-dynamic VALUE: dyn promises adopt, everything * else takes JS's one-hop non-thenable await and answers itself (+1). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 046aebb38..6e5d9856e 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -827,6 +827,19 @@ ScrDyn *scr_await_dyn_value_settled(ScrDyn *value) { return value->kind == SCR_DYN_PROMISE ? scr_await_dyn(value->v.promise) : scr_dyn_retain(value); } +static ScrDyn *scr_dyn_promise_payload(ScrPromise *p); + +/* The same for a native stackless body: the suspension has taken the turn + * scr_await_dyn would take, so the extraction takes none. */ +ScrDyn *scr_coro_await_dyn_value(ScrDyn *value) { + if (value->kind != SCR_DYN_PROMISE) return scr_dyn_retain(value); + ScrPromise *p = value->v.promise; + if (!scr_promise_settled_now(p)) + scr_trap("scriptc: internal error: resumed before awaited promise settled\n"); + if (scr_promise_is_rejected(p)) return NULL; + return scr_dyn_promise_payload(p); +} + /* ── process warnings (emitWarning + the 'warning' event) ───────────── * Gated with the rest of this TU (a deprecation-emitting unit's gate * must imply the dynAsync link). Listeners are dyn functions; the diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index f8dd2b79b..084a1d50d 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -441,8 +441,7 @@ ScrDyn *scr_buffer_constructor_value(void) { 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_attrs_at(table, scr_dyn_obj_count(table) - 1, false, false, 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++) { diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index daeb47c60..0c4bee4ee 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1757,11 +1757,11 @@ ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const if (!scr_fs_encoding_chk(opts)) return NULL; bool utf8 = true; if (opts->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - if (scr_fs_dyn_absent(e->value)) continue; - if (strcmp(e->key, "encoding") == 0) { - if (!scr_fs_utf8_encoding(e->value)) utf8 = false; + for (size_t i = 0; i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + if (scr_fs_dyn_absent(e.value)) continue; + if (strcmp(e.key, "encoding") == 0) { + if (!scr_fs_utf8_encoding(e.value)) utf8 = false; continue; } utf8 = false; /* an unmodeled effectful option */ diff --git a/packages/runtime/src/scr_closure.h b/packages/runtime/src/scr_closure.h index cfca90313..89f0378af 100644 --- a/packages/runtime/src/scr_closure.h +++ b/packages/runtime/src/scr_closure.h @@ -82,6 +82,24 @@ void scr_box_set_f64(ScrBox *b, double v); void scr_box_set_bool(ScrBox *b, bool v); void scr_box_set_ref(ScrBox *b, void *v); /* releases old, owns new */ +/* The bits of ScrClosure.function_kind. The LLVM backend stores them too: + * CLOSURE_KIND in packages/compiler/src/backend/llvm/common.ts mirrors this + * list, and runtime-registries.test.ts checks that the two agree and that + * the bits are distinct. The low two bits (SCR_CLOSURE_KIND_CALL_MASK) are + * the function's call kind: 0 ordinary, 1 generator, 2 async, 3 async + * generator. */ +#define SCR_CLOSURE_KIND_GENERATOR 1u +#define SCR_CLOSURE_KIND_ASYNC 2u +#define SCR_CLOSURE_KIND_CALL_MASK 3u +#define SCR_CLOSURE_KIND_OWN_PROTOTYPE 4u /* an ordinary function with its own prototype */ +#define SCR_CLOSURE_KIND_CHECKED_ADAPTER 8u /* a checked callable adapter: the callable is in caps[0] */ +/* 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_CLOSURE_KIND_MASK. */ +#define SCR_CLOSURE_KIND_RECEIVER_FREE 16u +#define SCR_CLOSURE_KIND_MASK 15u + typedef struct ScrClosure { size_t rc; /* SIZE_MAX = immortal (interned top-level function value) */ void *fn; @@ -98,7 +116,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]; bit 4: SCR_FN_RECEIVER_FREE */ + uint32_t function_kind; /* SCR_CLOSURE_KIND_* bits */ /* 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,13 +135,6 @@ 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 18c715cb2..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_v46(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 492d57d82..e11ffd3af 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -507,8 +507,8 @@ static bool scr_console_holds_buffer(const ScrDyn *d) { if (scr_console_holds_buffer(d->v.arr.items[i])) return true; return false; case SCR_DYN_OBJ: - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDyn *value = d->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + const ScrDyn *value = scr_dyn_obj_value_at(d, i); if (value && scr_console_holds_buffer(value)) return true; } return false; @@ -626,7 +626,7 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { return keys; } if (item->kind != SCR_DYN_OBJ) return NULL; - size_t len = item->v.obj.len; + size_t len = scr_dyn_obj_count(item); const char **names = calloc(len ? len : 1, sizeof *names); size_t *lens = calloc(len ? len : 1, sizeof *lens); size_t *order = calloc(len ? len : 1, sizeof *order); @@ -634,10 +634,10 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { if (!names || !lens || !order || !keys) scr_trap("scriptc: out of memory\n"); size_t n = 0; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *ent = &item->v.obj.entries[i]; - if (!ent->enumerable) continue; - names[n] = ent->key; - lens[n] = ent->key_len; + ScrDynProp prop = scr_dyn_obj_prop_at(item, i); + if (!prop.enumerable) continue; + names[n] = prop.key; + lens[n] = prop.key_len; n++; } scr_table_key_order(names, lens, n, order); @@ -652,16 +652,14 @@ static ScrStr **scr_table_own_keys(const ScrDyn *item, size_t *count) { /* item[key] when it is an own property; NULL otherwise. +1. */ static ScrDyn *scr_table_own_value(const ScrDyn *item, const ScrStr *key) { if (item->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < item->v.obj.len; i++) { - const ScrDynEntry *ent = &item->v.obj.entries[i]; - if (ent->key_len != key->len || memcmp(ent->key, key->data, key->len) != 0) continue; - if (ent->accessor) { - scr_table_fence("console.table rows with accessor properties have no lowering"); - return NULL; - } - return scr_dyn_retain(ent->value); + size_t i = scr_dyn_obj_index_of(item, key->data, key->len); + if (i == SIZE_MAX) return NULL; + ScrDynProp prop = scr_dyn_obj_prop_at(item, i); + if (prop.accessor) { + scr_table_fence("console.table rows with accessor properties have no lowering"); + return NULL; } - return NULL; + return scr_dyn_retain(prop.value); } if (item->kind == SCR_DYN_ARR || item->kind == SCR_DYN_BYTES) { size_t len = item->kind == SCR_DYN_ARR ? item->v.arr.len : item->v.bytes->len; diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 92392897a..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_v46(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_cycle.c b/packages/runtime/src/scr_cycle.c index 0d9df7106..a7157f9af 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -199,6 +199,8 @@ typedef struct { * depth budget covers every participating destructor. Queued zero-count * objects still own their children until their callback runs. */ #define SCR_DESTROY_DEPTH 64 +/* How deep the collector's walks recurse (see "the walks" below). */ +#define SCR_CYC_WALK_DEPTH 64 #ifdef SCR_WORKERS /* Worker executables keep the collector's per-thread state in one * thread-local block: on Darwin every distinct thread-local costs a call @@ -218,7 +220,7 @@ typedef struct { ScrVec white; ScrVec xgen; ScrVec xgen_edges; - void *gray_holder; + void *gray_holders[SCR_CYC_WALK_DEPTH]; ScrVec pub_holes[SCR_CYC_NGENS]; unsigned gen_limit; size_t live_after[SCR_CYC_NGENS]; @@ -260,7 +262,7 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD}; #define scr_white (scr_cyc_thread.white) #define scr_xgen (scr_cyc_thread.xgen) #define scr_xgen_edges (scr_cyc_thread.xgen_edges) -#define scr_gray_holder (scr_cyc_thread.gray_holder) +#define scr_gray_holders (scr_cyc_thread.gray_holders) #define scr_pub_holes (scr_cyc_thread.pub_holes) #define scr_gen_limit (scr_cyc_thread.gen_limit) #define scr_cyc_live_after (scr_cyc_thread.live_after) @@ -475,11 +477,13 @@ static SCR_TL ScrVec scr_xgen; /* Edges from a gray object into a generation the pass does not walk, as * (holder, target) pairs, recorded by markGray (each gray object is traced - * there exactly once, so each edge appears once), and the object markGray is - * tracing. */ + * there exactly once, so each edge appears once), and the objects markGray is + * tracing, by walk depth: an edge's holder is the entry one level above the + * depth its visit sees. Nothing is restored after an object's children are + * traced, so markGray ends in a tail call to the trace. */ #ifndef SCR_WORKERS static SCR_TL ScrVec scr_xgen_edges; -static SCR_TL void *scr_gray_holder; +static SCR_TL void *scr_gray_holders[SCR_CYC_WALK_DEPTH]; #endif /* Objects above this generation are invisible to the pass in flight. */ @@ -1338,12 +1342,12 @@ void scr_cyc_on_release(void *obj) { ((child) == NULL || SCR_RC(child) == SCR_RC_IMMORTAL \ || SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit) -/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where - * an object is queued instead (scr_pending, or scr_restore_pending for the - * restoration, which can run inside scan) and traced from the queue. Trace - * callbacks forward that integer depth through their opaque context; it is - * never dereferenced. */ -#define SCR_CYC_WALK_DEPTH 64 +/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH (defined + * with the per-thread state, which it sizes), where an object is queued + * instead (scr_pending, or scr_restore_pending for the restoration, which + * can run inside scan) and traced from the queue. Trace callbacks forward + * that integer depth through their opaque context; it is never + * dereferenced. */ /* A walk visits a dense cycle-capable array's children itself: the present * dense elements, which are all scr_arr_trace_v visits when the array has @@ -1382,9 +1386,10 @@ SCR_CYC_ARRAY_WALK(scr_sb_array, scr_sb_visit) SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit) static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth); -/* An edge into a generation this pass does not walk, for settle. */ -static __attribute__((noinline)) void scr_mg_skip_edge(void *child) { - scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder); +/* An edge into a generation this pass does not walk, for settle, seen at + * `depth`. */ +static __attribute__((noinline)) void scr_mg_skip_edge(void *child, uintptr_t depth) { + scr_cyc_push_work(&scr_xgen_edges, scr_gray_holders[depth - 1]); scr_cyc_push_work(&scr_xgen_edges, child); } /* The first visit of an object: gray it and walk its children. */ @@ -1398,7 +1403,7 @@ static void scr_mg_visit(void *child, void *ctx) { if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) { - scr_mg_skip_edge(child); + scr_mg_skip_edge(child, (uintptr_t)ctx); return; } --SCR_RC(child); @@ -1438,11 +1443,9 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp scr_cyc_push_work(&scr_pending, obj); return; } - void *holder = scr_gray_holder; - scr_gray_holder = obj; + scr_gray_holders[depth] = obj; const ScrCycType *type = scr_cyc_type(h); SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, depth + 1); - scr_gray_holder = holder; } /* markGray from one (black) candidate, through the queue. */ static void scr_mark_gray_from(void *root) { @@ -1450,7 +1453,7 @@ static void scr_mark_gray_from(void *root) { while (scr_pending.n) { void *obj = scr_pending.v[--scr_pending.n]; const ScrCycType *type = scr_cyc_type(scr_cyc_hdr(obj)); - scr_gray_holder = obj; + scr_gray_holders[0] = obj; SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, 1); } } @@ -1474,7 +1477,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); @@ -1950,7 +1956,9 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { /* The list holds the dead set: doomed components, and what scan whitened * (black once a later outside root restored it), or, when the pass found * nothing externally referenced, every gray object. `doomed` says that - * every entry is a doomed component's already. */ + * every entry is a doomed component's: every candidate and every holder + * of a recorded edge is about to be freed, and the entries can still be + * gray (see "Components" in scr_cyc_pass). */ for (size_t i = 0; i < scr_white.n && !doomed; i++) { void *obj = scr_white.v[i]; ScrCycHdr *h = scr_cyc_hdr(obj); @@ -1971,7 +1979,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { * what changes them). A full pass skips no edge, so it pins nothing. */ scr_xgen.n = 0; for (size_t i = 0; i < scr_xgen_edges.n; i += 2) { - if (scr_cyc_color(scr_cyc_hdr(scr_xgen_edges.v[i])) != SCR_CYC_DOOMED) continue; + if (!doomed && scr_cyc_color(scr_cyc_hdr(scr_xgen_edges.v[i])) != SCR_CYC_DOOMED) continue; void *target = scr_xgen_edges.v[i + 1]; SCR_RC(target) += 1; /* pin across the teardowns */ scr_cyc_push_work(&scr_xgen, target); @@ -2002,7 +2010,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { * genuine outside reference and Bacon-Rajan's own reasoning retires the * candidate. Re-buffering costs a walk, never correctness: an object that * is truly live survives the next pass at its level and is dropped then. */ - if (gen_limit < SCR_CYC_OLD) { + if (gen_limit < SCR_CYC_OLD && !doomed) { for (size_t i = 0; i < scr_cands.n; i++) { void *obj = scr_cands.v[i]; ScrCycHdr *h = scr_cyc_hdr(obj); @@ -2110,6 +2118,17 @@ static size_t scr_cyc_pass_done(size_t freed) { return freed; } +/* Color the doomed components' entries of the gray list: every entry + * outside the referenced components' ranges. */ +static SCR_CYC_COMPACT void scr_cyc_doom_components(void) { + size_t from = 0; + for (size_t k = 0; k <= scr_live_ranges.n; k += 2) { + size_t end = k < scr_live_ranges.n ? (uintptr_t)scr_live_ranges.v[k] : scr_white.n; + for (size_t j = from; j < end; j++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[j]), SCR_CYC_DOOMED); + if (k < scr_live_ranges.n) from = (uintptr_t)scr_live_ranges.v[k + 1]; + } +} + static size_t scr_cyc_pass(unsigned gen_limit) { if (scr_collecting) return 0; scr_cyc_route_bypassed(); @@ -2190,9 +2209,15 @@ static size_t scr_cyc_pass(unsigned gen_limit) { * cannot add one: its objects would be referencing this component, and * those references are still counted. scan would whiten exactly this * set, so it is doomed at once and scan runs only over the components - * that are externally referenced, if any. A limited pass keeps the single - * whole-pass decision below (its untaken candidates cannot carry the - * component number). */ + * that are externally referenced, if any. A doomed component's entries + * stay gray, outside the referenced components' ranges, until something + * reads their color: settle colors them while it filters the list, and + * needs no colors when every component is doomed; a scan from the + * candidates colors them first (scr_cyc_doom_components); and with a + * dispose observer installed they are colored here, because a weak map's + * teardown tells a collected owner by its color. A limited pass keeps + * the single whole-pass decision below (its untaken candidates cannot + * carry the component number). */ bool split = scr_cyc_take == NULL; uint32_t comp = 0; for (size_t i = 0; i < scr_cands.n; i++) { @@ -2205,8 +2230,11 @@ static size_t scr_cyc_pass(unsigned gen_limit) { if (!split) continue; size_t end = scr_white.n; if (scr_comp_referenced == 0 && end < SCR_CYC_GRAY_LIST_MAX) { - for (size_t j = start; j < end; j++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[j]), SCR_CYC_DOOMED); - } else if (scr_live_ranges.n && (uintptr_t)scr_live_ranges.v[scr_live_ranges.n - 1] == start) { + if (scr_weak_dispose_hook) + for (size_t j = start; j < end; j++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[j]), SCR_CYC_DOOMED); + continue; + } + if (scr_live_ranges.n && (uintptr_t)scr_live_ranges.v[scr_live_ranges.n - 1] == start) { scr_live_ranges.v[scr_live_ranges.n - 1] = (void *)(uintptr_t)end; } else { scr_cyc_push_work(&scr_live_ranges, (void *)(uintptr_t)start); @@ -2252,7 +2280,9 @@ static size_t scr_cyc_pass(unsigned gen_limit) { } /* Otherwise scan walks from the candidates. Keep only the doomed - * components' entries; scan appends what it whitens. */ + * components' entries, colored first so that scan passes over them; scan + * appends what it whitens. */ + if (split) scr_cyc_doom_components(); if (!split) { scr_white.n = 0; } else if (scr_live_ranges.n) { @@ -2350,7 +2380,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++) { @@ -2358,6 +2390,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..ffac1f802 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) @@ -56,10 +63,10 @@ static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { else if (n->v.obj.source_access && !n->v.obj.source_identity) want = "a plain object"; else { - for (size_t i = 0; i < n->v.obj.len && !want; i++) { - const ScrDynEntry *e = &n->v.obj.entries[i]; - if (e->accessor) want = "a plain object (this value has accessor properties)"; - else if (!e->writable || !e->enumerable || !e->configurable) + for (size_t i = 0; i < scr_dyn_obj_count(n) && !want; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(n, i); + if (e.accessor) want = "a plain object (this value has accessor properties)"; + else if (!e.writable || !e.enumerable || !e.configurable) want = "a plain object (this value has non-default property attributes)"; } } @@ -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 ef33fa2a7..3a1e83308 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -129,17 +129,30 @@ typedef enum { * lowerings (the http/net receiver surface); new kinds join by adding a * tag + an ops registration in their owning unit. * - * SCR_DYNH_TAGS is the one list of tags: X(NAME, value) declares - * SCR_DYNH_NAME. Rows are sorted by name and every value is explicit and - * permanent: the LLVM backend hardcodes some of them (DYN_HANDLE_TAG_NUM in - * packages/compiler/src/backend/llvm/dyn.ts). A new tag goes in its sorted - * place with the next value after the largest one in use, below - * SCR_DYNH_CAPACITY. Two changes that add tags therefore edit different - * lines and merge without a textual conflict; when they picked the same - * value, scr_dynh_tags_distinct below has a duplicate case label and every - * runtime unit fails to compile, and a value at or above the capacity - * fails its _Static_assert. packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts - * checks the same properties and that the backend's numbers match. */ + * Two lists declare the tags. SCR_DYNH_TAGS holds the PINNED tags: X(NAME, + * value) declares SCR_DYNH_NAME with an explicit, permanent value, rows + * sorted by name. SCR_DYNH_NEW_TAGS holds tags added since the last pin, by + * NAME ONLY: Y(NAME), rows sorted by name. A new tag goes in its sorted place + * in SCR_DYNH_NEW_TAGS, so a branch never picks a number and two branches + * that add tags never collide; the enum numbers the new tags after the + * largest pinned value, in list order. Values are an ABI only between the + * compiler and the runtime pack of one build, so a new tag added before + * another one in name order may renumber it; nothing outside the build + * depends on it. + * + * Pin a new tag (move it to SCR_DYNH_TAGS with the value it has now: the + * largest pinned value + 1 for the first row of SCR_DYNH_NEW_TAGS, + 2 for + * the second, and so on) only when the LLVM backend needs its number: DYN_HANDLE_TAG_NUM and + * TEXT_CODEC_TAG_NUM in packages/compiler/src/backend/llvm/dyn.ts may only + * name pinned tags. Pin the new tags in name order, oldest first, in a change + * of its own, so their values do not move. + * + * Checks: a pinned value at or above SCR_DYNH_CAPACITY fails its + * _Static_assert, two tags with one value are a duplicate case label in + * scr_dynh_tags_distinct below, and the header does not compile. + * dyn-handle-tags.test.ts checks the same properties by name, that both lists + * are sorted, that every value below the largest pinned one is a tag or + * listed in SCR_DYNH_UNASSIGNED, and that the backend's numbers match. */ #define SCR_DYNH_TAGS(X) \ X(ABORT_CONTROLLER, 15) /* native static-fetch AbortController */ \ X(ABORT_SIGNAL, 8) /* native static-fetch AbortSignal */ \ @@ -189,6 +202,7 @@ typedef enum { 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 */ \ @@ -224,24 +238,55 @@ typedef enum { X(WEB_WRITER, 75) /* WritableStreamDefaultWriter (scr_fetch.c) */ \ X(WORKER, 35) +/* Tags added since the last pin, by name only (see above). */ +#define SCR_DYNH_NEW_TAGS(Y) + +/* Values below the largest pinned one that no tag holds, each with the + * reason. Never reuse one without removing it here. */ +#define SCR_DYNH_UNASSIGNED(Z) \ + Z(63) /* skipped: claimed by a branch that took another number */ + +/* One member per pinned tag, sized value + 1, so its size is the largest + * pinned value + 1: the new tags are numbered from there. Never instantiated. */ +union ScrDynhPinnedSpan { +#define SCR_DYNH_SPAN_ROW(name, value) char name[(value) + 1]; + SCR_DYNH_TAGS(SCR_DYNH_SPAN_ROW) +#undef SCR_DYNH_SPAN_ROW +}; + typedef enum { #define SCR_DYNH_ENUM_ROW(name, value) SCR_DYNH_##name = value, SCR_DYNH_TAGS(SCR_DYNH_ENUM_ROW) #undef SCR_DYNH_ENUM_ROW + SCR_DYNH__LAST_PINNED = (int)sizeof(union ScrDynhPinnedSpan) - 1, +#define SCR_DYNH_NEW_ROW(name) SCR_DYNH_##name, + SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_ROW) +#undef SCR_DYNH_NEW_ROW } ScrDynHandleTag; #define SCR_DYNH_RANGE_ROW(name, value) \ _Static_assert((value) < SCR_DYNH_CAPACITY, "SCR_DYNH_" #name " is not below SCR_DYNH_CAPACITY"); SCR_DYNH_TAGS(SCR_DYNH_RANGE_ROW) #undef SCR_DYNH_RANGE_ROW +#define SCR_DYNH_NEW_RANGE_ROW(name) \ + _Static_assert(SCR_DYNH_##name < SCR_DYNH_CAPACITY, "SCR_DYNH_" #name " is not below SCR_DYNH_CAPACITY"); +SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_RANGE_ROW) +#undef SCR_DYNH_NEW_RANGE_ROW -/* Never called: a case label per tag, so two tags with one value are a - * duplicate case value and the header does not compile. */ +/* Never called: a case label per tag and per unassigned value, so two tags + * with one value, or a tag on an unassigned value, are a duplicate case + * value and the header does not compile. */ static inline void scr_dynh_tags_distinct(ScrDynHandleTag tag) { - switch (tag) { + switch ((int)tag) { #define SCR_DYNH_CASE_ROW(name, value) case SCR_DYNH_##name: SCR_DYNH_TAGS(SCR_DYNH_CASE_ROW) #undef SCR_DYNH_CASE_ROW +#define SCR_DYNH_NEW_CASE_ROW(name) case SCR_DYNH_##name: + SCR_DYNH_NEW_TAGS(SCR_DYNH_NEW_CASE_ROW) +#undef SCR_DYNH_NEW_CASE_ROW +#define SCR_DYNH_UNASSIGNED_CASE_ROW(value) case value: + SCR_DYNH_UNASSIGNED(SCR_DYNH_UNASSIGNED_CASE_ROW) +#undef SCR_DYNH_UNASSIGNED_CASE_ROW break; } } @@ -383,6 +428,13 @@ typedef struct ScrDynTypedRecordOps { * optional key can change after the record is boxed. NULL in every other * table. */ const struct ScrDynTypedRecordOps *(*select)(void *rec); + /* Stores `value` into present field `index` in place when it fits the + * field's type and answers true (also when the conversion itself threw, + * which has then raised the write's error); answers false with no side + * effects otherwise, leaving the snapshot path to raise its diagnostic. + * NULL when the program never writes through a dynamic view, and for a + * field whose storage it cannot replace (an inline union). */ + bool (*set)(void *rec, size_t index, const ScrDyn *value); } ScrDynTypedRecordOps; /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ @@ -393,6 +445,7 @@ 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 @@ -468,9 +521,16 @@ struct ScrDyn { struct { ScrSym *value; void (*release)(ScrSym *); ScrStr *(*render)(ScrSym *); ScrStr *(*description)(ScrSym *); } symbol; struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; unsigned char *presence; struct ScrDynRegion *region; } arr; /* owned; NULL presence means dense; region: see ScrDynRegion */ struct { - size_t len; - size_t cap; - ScrDynEntry *entries; + size_t len; /* own string-keyed properties, in both modes */ + size_t cap; /* capacity of entries (dictionary mode) or slots (layout mode) */ + /* Layout mode (`layout` non-NULL): slots[i] holds the value of the + * property layout->keys[i] names, a writable, enumerable, + * configurable data property. Dictionary mode (`layout` NULL): + * entries hold keys, values and attributes. See ScrLayout. */ + union { + ScrDynEntry *entries; + struct ScrDyn **slots; + }; /* Optional identity of the typed record that produced this ordinary * snapshot. Empty records and callback interfaces can retain their * source across repeated static-to-dynamic crossings. */ @@ -478,7 +538,7 @@ struct ScrDyn { * interfaces may request a fresh snapshot with the true arm. */ void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); - struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ + struct ScrDynObjectIndex *index; /* dictionary mode: optional wide-object lookup; owns no values */ /* On a runtime-built class prototype (e.g. url.Url.prototype) and * on a compiled class's prototype object: the constructor name * (static or immortal text) util.inspect prints for objects @@ -487,19 +547,13 @@ struct ScrDyn { * exits, and those clear it before release; reuse of a freed object * resets it (scr_dyn_alloc). */ const char *class_name; - 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: - * their runtime is built with SCR_JS_TIER, where every allocation and - * every change to the key set resets it. Other programs never look - * at it, so it may hold anything there. */ - struct ScrJsShape *shape; + struct ScrDynRegion *region; /* see ScrDynRegion */ + /* The object's layout in layout mode, NULL in dictionary mode. The + * dynamic tier's inline caches key on it. */ + const struct ScrLayout *layout; + /* Layout mode: bit i is the collector's snapshot of whether slot i's + * value is traced (ScrDynEntry.value_traced in dictionary mode). */ + size_t slots_traced; } obj; /* owned */ /* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig` * and `name` are static compiler-emitted literals (never freed); name @@ -578,10 +632,155 @@ _Static_assert(offsetof(ScrDyn, v.typed_ref.casts) == 16 + 8 * sizeof(void *), _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref) && sizeof(((ScrDyn *)0)->v.obj) <= sizeof(((ScrDyn *)0)->v.typed_ref), "dyn region links must not grow the node"); -/* Dynamic-tier inline caches read the shape word in place (64-bit). */ -_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.shape) == 80, "v.obj.shape offset"); +/* Dynamic-tier inline caches read the layout word and slots in place (64-bit). */ +_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.layout) == 80, "v.obj.layout offset"); +_Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.slots) == 32, "v.obj.slots offset"); _Static_assert(sizeof(void *) != 8 || offsetof(ScrDyn, v.obj.region) == 72, "v.obj.region offset"); +/* ── layouts ─────────────────────────────────────────────────────────── + * An ordinary object (SCR_DYN_OBJ) is in one of two modes. + * + * Layout mode: v.obj.layout names the object's key sequence, a node of the + * thread's interned transition tree, and v.obj.slots holds one value per + * key (8 bytes per property). Every property of a layout-mode object is a + * writable, enumerable, configurable data property; objects of one key + * sequence share one layout, so a layout pointer proves which key sits at + * each slot and which keys the object lacks (the dynamic tier's inline + * caches key on it). New objects start in layout mode at the thread's + * empty layout. + * + * Dictionary mode: v.obj.layout is NULL and v.obj.entries holds keys, + * values and attributes (ScrDynEntry). An object moves there, once and for + * good, when it gets an accessor, a non-default attribute, a delete, more + * than SCR_LAYOUT_MAX_KEYS keys, when the thread's layout budgets are + * spent, when it is published to other threads, and whenever code that + * reads entries directly touches it (scr_dyn_obj_dict_entries): the conversion + * is unobservable, so code that has not been taught layouts stays correct. + * + * Layouts belong to one thread and live until its runtime state is torn + * down; they are never owned by a parse region. Each holds one reference + * to its key's bytes (scr_json.c's shared key storage). */ +#define SCR_LAYOUT_MAX_KEYS (8 * sizeof(size_t)) + +typedef struct ScrLayoutKey { + char *key; /* shared key bytes; one reference belongs to the layout that added it */ + uint32_t len; + uint32_t hash; /* low bits of scr_key_hash */ +} ScrLayoutKey; + +typedef struct ScrLayout { + const ScrLayoutKey *keys; /* keys[0..count); shared along a chain of layouts */ + uint32_t count; + uint32_t lookup_mask; /* capacity - 1 of `lookup`; 0 while it has none */ + uint8_t *lookup; /* lazily built hash index: slot + 1, 0 = empty */ + const struct ScrLayout *parent; + struct ScrLayout *last_child; /* the most recent transition from here */ + struct ScrLayoutKeys *store; /* the key array `keys` lives in */ + struct ScrLayout *all_next; + bool owns_store; +} ScrLayout; + +/* The index of `key` in layout `layout`, or UINT32_MAX. */ +uint32_t scr_layout_find(const ScrLayout *layout, const char *key, size_t key_len); +/* The layout that appends `key` (borrowed bytes) to `layout`, created on + * first use; NULL past SCR_LAYOUT_MAX_KEYS or the thread's budgets. */ +const ScrLayout *scr_layout_add(const ScrLayout *layout, const char *key, size_t key_len); +/* The thread's empty layout. */ +const ScrLayout *scr_layout_root(void); +/* Moves a layout-mode object to dictionary mode (no-op otherwise). */ +void scr_dyn_to_dictionary(ScrDyn *object); + +/* v.obj.entries of an object in dictionary mode, converting a layout-mode + * object first: code that reads or writes entries directly goes through + * this. */ +static inline __attribute__((always_inline)) ScrDynEntry *scr_dyn_obj_dict_entries(const ScrDyn *object) { + if (__builtin_expect(object->v.obj.layout != NULL, 0)) scr_dyn_to_dictionary((ScrDyn *)object); + return object->v.obj.entries; +} + +/* scr_dyn_obj_key_at and scr_dyn_obj_value_at out of line, for emitted + * code (borrowed results). */ +const char *scr_dyn_member_key(const ScrDyn *object, size_t i); +size_t scr_dyn_member_key_len(const ScrDyn *object, size_t i); +ScrDyn *scr_dyn_member_value(const ScrDyn *object, size_t i); + +/* ── own properties of ordinary objects ────────────────────────────── + * The accessors are always inlined, so a binary that never sees an + * ordinary object links no out-of-line copies. Code outside the object core (scr_json.c and scr_js_object.c) reads and + * adjusts an SCR_DYN_OBJ's own string-keyed properties through these + * accessors, never through v.obj.entries or v.obj.slots, so they work in + * both modes. Index i runs over the properties in creation order, + * 0 <= i < scr_dyn_obj_count. Keys and values are borrowed: they stay + * valid until the object changes. */ +typedef struct { + const char *key; + size_t key_len; + ScrDyn *value; /* data property; the undefined value for an accessor */ + ScrDyn *getter; /* accessor property; NULL when absent */ + ScrDyn *setter; /* accessor property; NULL when absent */ + bool accessor; + bool writable; + bool enumerable; + bool configurable; +} ScrDynProp; + +static inline __attribute__((always_inline)) size_t scr_dyn_obj_count(const ScrDyn *o) { + return o->v.obj.len; +} + +static inline __attribute__((always_inline)) const char *scr_dyn_obj_key_at(const ScrDyn *o, size_t i, size_t *key_len) { + const ScrLayout *layout = o->v.obj.layout; + if (layout) { + *key_len = layout->keys[i].len; + return layout->keys[i].key; + } + *key_len = o->v.obj.entries[i].key_len; + return o->v.obj.entries[i].key; +} + +/* The value of data property i (borrowed); the undefined value for an + * accessor property, whose functions scr_dyn_obj_prop_at answers. */ +static inline __attribute__((always_inline)) ScrDyn *scr_dyn_obj_value_at(const ScrDyn *o, size_t i) { + return o->v.obj.layout ? o->v.obj.slots[i] : o->v.obj.entries[i].value; +} + +static inline __attribute__((always_inline)) ScrDynProp scr_dyn_obj_prop_at(const ScrDyn *o, size_t i) { + const ScrLayout *layout = o->v.obj.layout; + if (layout) + return (ScrDynProp){layout->keys[i].key, layout->keys[i].len, o->v.obj.slots[i], NULL, NULL, + false, true, true, true}; + const ScrDynEntry *e = &o->v.obj.entries[i]; + return (ScrDynProp){e->key, e->key_len, e->value, e->getter, e->setter, + e->accessor, e->writable, e->enumerable, e->configurable}; +} + +/* The index of own property `key`, or SIZE_MAX. A linear scan in + * dictionary mode: for code that looks up options and headers, not for hot + * property reads. */ +static inline size_t scr_dyn_obj_index_of(const ScrDyn *o, const char *key, size_t key_len) { + if (o->v.obj.layout) { + uint32_t i = scr_layout_find(o->v.obj.layout, key, key_len); + return i == UINT32_MAX ? SIZE_MAX : i; + } + for (size_t i = 0; i < o->v.obj.len; i++) { + const ScrDynEntry *e = &o->v.obj.entries[i]; + if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) return i; + } + return SIZE_MAX; +} + +/* Sets the attributes of property i (`writable` is ignored for an + * accessor property): builtin objects the runtime builds restrict their + * members this way right after adding them. */ +static inline void scr_dyn_obj_set_attrs_at(ScrDyn *o, size_t i, bool writable, bool enumerable, + bool configurable) { + if (o->v.obj.layout && writable && enumerable && configurable) return; + ScrDynEntry *e = &scr_dyn_obj_dict_entries(o)[i]; + if (!e->accessor) e->writable = writable; + e->enumerable = enumerable; + e->configurable = configurable; +} + /* ── intact parse regions ───────────────────────────────────────────── * Every heap dyn node carries a cycle header, so an ordinary release that * leaves an array or object alive buffers it as a possible cycle root, and @@ -746,6 +945,27 @@ ScrDyn *scr_dyn_get_own_property_symbols(const ScrDyn *value); /* The own property stored under a symbol key on an object (borrowed), or * NULL when it has none. */ ScrDynEntry *scr_dyn_own_symbol_entry(const ScrDyn *object, const ScrDyn *key); +/* The own symbol-keyed property `key` of `object` as a property view + * (scr_dyn_obj_prop_at; `key` and `key_len` name its private storage + * key); false when there is none. */ +static inline bool scr_dyn_own_symbol_prop(const ScrDyn *object, const ScrDyn *key, ScrDynProp *out) { + const ScrDynEntry *e = scr_dyn_own_symbol_entry(object, key); + if (!e) return false; + *out = (ScrDynProp){e->key, e->key_len, e->value, e->getter, e->setter, + e->accessor, e->writable, e->enumerable, e->configurable}; + return true; +} +/* Copies own property i of `source` to `target` (scr_dyn_copy_entry). */ +static inline void scr_dyn_copy_prop_at(ScrDyn *target, const ScrDyn *source, size_t i) { + if (source->v.obj.layout) { + ScrDynProp prop = scr_dyn_obj_prop_at(source, i); + ScrDynEntry entry = {(char *)prop.key, prop.key_len, prop.value, NULL, NULL, + false, false, false, true, true, true}; + scr_dyn_copy_entry(target, source, &entry); + return; + } + scr_dyn_copy_entry(target, source, &source->v.obj.entries[i]); +} /* The private storage name of a symbol key (+1). With it, * scr_dyn_symbol_init_own gives an object that has no symbol properties * yet its first one, a writable, enumerable, configurable data property @@ -1095,8 +1315,10 @@ bool scr_dyn_typed_ref_is( const ScrDyn *d, const char *type_key, size_t type_key_len); bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key); void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */ +void *scr_dyn_typed_ref_prefix_view(const ScrDyn *d); /* +1 or NULL */ ScrDyn *scr_dyn_class_view_unavailable(void *ptr); ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */ +ScrDyn *scr_dyn_typed_ref_peek(const ScrDyn *d); /* +1 */ /* What string conversion renders for a capsule (+1): its materialized * view, or, for an EventTarget or Event subclass without its own * toString, the "[object EventTarget]" / "[object Event]" string of its @@ -1284,6 +1506,10 @@ bool scr_dyn_is_array_prototype(const ScrDyn *d); * non-object kinds throw Node's catchable TypeErrors (strict-mode * wording). All three operands BORROWED (the value is retained in). */ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); +/* The same in sloppy-mode code: an assignment strict mode refuses with a + * TypeError is ignored. */ +void scr_dyn_key_set_sloppy(ScrDyn *recv, ScrStr *key, ScrDyn *value); +void scr_dyn_key_set_computed_sloppy(ScrDyn *recv, ScrDyn *key, ScrDyn *value); /* A named (non-index) property of an extensible, unfrozen ARR, stored as * scr_dyn_key_set stores it. Takes ownership of value. */ void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); @@ -1299,6 +1525,18 @@ void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol); * 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); @@ -1607,6 +1845,10 @@ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn * /* 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 @@ -1965,7 +2207,7 @@ void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx); long scr_dyn_live_count(void); #endif -/* Checked-object primitives the dynamic tier's shapes and caches build on +/* Checked-object primitives the dynamic tier's inline caches build on * (scr_js_object.c). They exist only in the SCR_JS_TIER build of * scr_json.c, which programs with dynamic-tier code link. */ /* The builtin Object.prototype node, or NULL before its first use. */ @@ -1973,12 +2215,9 @@ 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); -/* Appends an own data property (writable, enumerable, configurable) that - * `obj` does not have, sharing `key`'s bytes and taking ownership of - * `value`, exactly as an ordinary insertion would; leaves the shape reset. */ -void scr_dyn_tier_append(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value); +/* Appends to a layout-mode object the property that `next`, a child of + * its layout, adds, taking ownership of `value`, exactly as an ordinary + * insertion would. */ +void scr_dyn_tier_append(ScrDyn *obj, const ScrLayout *next, ScrDyn *value); #endif /* SCR_DYN_H */ diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 0647a65c0..b176e1a42 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -1441,7 +1441,7 @@ static void bc_teardown(void) { static ScrDyn *bc_new_class(int kind, ScrDyn *parent) { ScrClosure *closure = scr_closure_new(NULL, 1); - closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE; closure->caps[0] = scr_box_new(SCR_BOX_F64); scr_box_set_f64(closure->caps[0], kind); ScrDyn *ctor = scr_dyn_new_func(closure, bc_construct, 1, "native:events", bc_names[kind]); diff --git a/packages/runtime/src/scr_dyn_handle_props.c b/packages/runtime/src/scr_dyn_handle_props.c index c7de963f0..88677836f 100644 --- a/packages/runtime/src/scr_dyn_handle_props.c +++ b/packages/runtime/src/scr_dyn_handle_props.c @@ -60,17 +60,17 @@ static ScrDyn *hp_props_of(void *child) { return props; } -static ScrDynEntry *hp_entry(const ScrDyn *props, const char *key, size_t key_len) { - if (!props) return NULL; - for (size_t i = 0; i < props->v.obj.len; i++) { - ScrDynEntry *e = &props->v.obj.entries[i]; - if (e->key_len == key_len && !memcmp(e->key, key, key_len)) return e; - } - return NULL; +/* Own property `key` of the table object, in *out; false when absent. */ +static bool hp_entry(const ScrDyn *props, const char *key, size_t key_len, ScrDynProp *out) { + if (!props) return false; + size_t i = scr_dyn_obj_index_of(props, key, key_len); + if (i == SIZE_MAX) return false; + *out = scr_dyn_obj_prop_at(props, i); + return true; } static bool hp_has(const ScrDyn *props, const char *key, size_t key_len) { - return hp_entry(props, key, key_len) != NULL; + return props && scr_dyn_obj_index_of(props, key, key_len) != SIZE_MAX; } /* A property the program added, read with the child as `this`: an @@ -78,10 +78,10 @@ static bool hp_has(const ScrDyn *props, const char *key, size_t key_len) { * it (`Object.defineProperty(child, "x", { get() { return this.pid; } })`). * +1, or NULL with the exception pending. */ static ScrDyn *hp_read(ScrDyn *props, ScrDyn *self, const char *key, size_t key_len) { - ScrDynEntry *e = hp_entry(props, key, key_len); - if (!e || !e->accessor) return scr_dyn_obj_read(props, key, key_len); - if (!e->getter) return scr_dyn_retain(scr_dyn_undefined()); - ScrDyn *getter = scr_dyn_retain(e->getter); + ScrDynProp e; + if (!hp_entry(props, key, key_len, &e) || !e.accessor) return scr_dyn_obj_read(props, key, key_len); + if (!e.getter) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(e.getter); scr_dyn_this_push_dyn(self); ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); scr_dyn_this_pop(); @@ -132,7 +132,8 @@ static ScrDyn *hp_get(void *h, const char *key, size_t key_len) { static bool hp_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { ScrDyn *props = hp_find(h); - ScrDynEntry *entry = hp_entry(props, key, key_len); + ScrDynProp found; + const ScrDynProp *entry = hp_entry(props, key, key_len, &found) ? &found : NULL; if (entry && entry->accessor) { /* An accessor's setter runs on the child. Without one the write throws * the strict-mode TypeError, as the other handle getters do. */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index e31359efd..7a9b1bc35 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -916,6 +916,117 @@ void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method, 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) { @@ -1367,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; @@ -1550,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); @@ -1616,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) { @@ -1801,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(); @@ -1975,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, @@ -2240,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; @@ -2299,7 +2357,8 @@ 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 */ } diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index d28707d5d..1b30c451d 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4821,14 +4821,14 @@ static ScrArr *sf_headers_convert_init(const ScrDyn *headers, bool *canonical_ou return converted; } if (headers->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - ScrStr *raw_name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + ScrStr *raw_name = scr_str_new(e.key, e.key_len); ScrStr *name_bytes = sf_header_value_bytestring(raw_name); ScrStr *name = name_bytes ? sf_latin1_to_utf8(name_bytes) : NULL; scr_str_release(name_bytes); scr_str_release(raw_name); - ScrDyn *source = scr_dyn_retain(e->value); + ScrDyn *source = scr_dyn_retain(e.value); ScrStr *raw_value = name ? scr_dyn_string_coerce_js(source) : NULL; scr_dyn_release(source); ScrStr *value_bytes = raw_value @@ -6088,11 +6088,11 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, } int8_t form = 0; if (headers->kind == SCR_DYN_OBJ) { - ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len); + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * scr_dyn_obj_count(headers)); form = 3; - for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *value = e->value; + for (size_t i = 0; i < scr_dyn_obj_count(headers) && form == 3; i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *value = e.value; size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1; for (size_t j = 0; j < count; j++) { const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value; @@ -6106,7 +6106,7 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, form = 0; break; } - scr_arr_push_ref(out, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(out, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(out, text); } } @@ -6666,27 +6666,27 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { return sf_reject_now(promise, "fetch failed"); } if (init && init->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < init->v.obj.len; i++) { - const ScrDynEntry *entry = &init->v.obj.entries[i]; - bool supported = sf_name(entry->key, entry->key_len, "method") || - sf_name(entry->key, entry->key_len, "headers") || - sf_name(entry->key, entry->key_len, "body") || - sf_name(entry->key, entry->key_len, "duplex") || - sf_name(entry->key, entry->key_len, "redirect") || - sf_name(entry->key, entry->key_len, "signal"); + for (size_t i = 0; i < scr_dyn_obj_count(init); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(init, i); + bool supported = sf_name(entry.key, entry.key_len, "method") || + sf_name(entry.key, entry.key_len, "headers") || + sf_name(entry.key, entry.key_len, "body") || + sf_name(entry.key, entry.key_len, "duplex") || + sf_name(entry.key, entry.key_len, "redirect") || + sf_name(entry.key, entry.key_len, "signal"); bool recognized_unsupported = - sf_name(entry->key, entry->key_len, "cache") || - sf_name(entry->key, entry->key_len, "credentials") || - sf_name(entry->key, entry->key_len, "dispatcher") || - sf_name(entry->key, entry->key_len, "integrity") || - sf_name(entry->key, entry->key_len, "keepalive") || - sf_name(entry->key, entry->key_len, "mode") || - sf_name(entry->key, entry->key_len, "priority") || - sf_name(entry->key, entry->key_len, "referrer") || - sf_name(entry->key, entry->key_len, "referrerPolicy") || - sf_name(entry->key, entry->key_len, "window"); - if (!supported && recognized_unsupported && entry->value && - entry->value->kind != SCR_DYN_UNDEF) { + sf_name(entry.key, entry.key_len, "cache") || + sf_name(entry.key, entry.key_len, "credentials") || + sf_name(entry.key, entry.key_len, "dispatcher") || + sf_name(entry.key, entry.key_len, "integrity") || + sf_name(entry.key, entry.key_len, "keepalive") || + sf_name(entry.key, entry.key_len, "mode") || + sf_name(entry.key, entry.key_len, "priority") || + sf_name(entry.key, entry.key_len, "referrer") || + sf_name(entry.key, entry.key_len, "referrerPolicy") || + sf_name(entry.key, entry.key_len, "window"); + if (!supported && recognized_unsupported && entry.value && + entry.value->kind != SCR_DYN_UNDEF) { return sf_reject_now(promise, "fetch failed"); } } diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c73ceb900..4f43892b4 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -60,6 +60,14 @@ void scr_gen_coro_finish(ScrFiber *self); ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, void (*drop_args)(void *), void (*settle)(ScrGen *, ScrPromise *)); +/* An async generator whose body is a switched coroutine, as + * scr_gen_new_stackless's: its steps run on the resumer's stack (the loop's + * after an await), its awaits suspend through scr_coro_await_prepare and + * scr_coro_hop_prepare, and its yields store OUT through scr_coro_yield_*. + * Native targets only. */ +ScrGen *scr_async_gen_new_stackless(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *), + void (*settle)(ScrGen *, ScrPromise *)); /* Runs when return() reaches the generator before its first resume, with * the packed arguments still alive: an iterator helper closes its source. */ void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)); @@ -74,8 +82,9 @@ void scr_gen_set_tag(ScrGen *g, const char *tag); void scr_gen_copy_tag(ScrGen *g, const ScrGen *from); const char *scr_gen_tag(const ScrGen *g); /* scr_gen_tag for units that also link without the generator runtime (a - * library links none): the generator runtime sets it at startup, and only - * then can a generator capsule exist. */ + * library links none): the generator runtime sets it when it creates a + * generator, so it is set whenever a generator capsule exists. Read it with + * a relaxed atomic load. */ extern const char *(*scr_gen_tag_reader)(const ScrGen *g); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index 3fb0fdcba..0284c31c4 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2047,10 +2047,10 @@ void scr_http_res_write_head_dyn(ScrHttpRes *r, double status, const ScrDyn *hea scr_dyn_arg_type_fail("headers", "an instance of Object", headers); return; } - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; - ScrStr *name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; + ScrStr *name = scr_str_new(e.key, e.key_len); if (v->kind == SCR_DYN_STR) { scr_http_res_set_header(r, name, v->v.str); } else if (v->kind == SCR_DYN_NUM) { @@ -6200,10 +6200,10 @@ static bool scr_http_dynh_req_set(void *h, const char *key, size_t key_len, cons * consecutive same-name pairs). Numbers format through String(n). NULL = * a value this surface cannot honestly carry (exception pending). */ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { - ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2); - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; + ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, scr_dyn_obj_count(headers) * 2); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1; for (size_t k = 0; k < reps; k++) { const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[k] : v; @@ -6218,7 +6218,7 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { scr_jb_puts(&b, "Invalid value \""); scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object"); scr_jb_puts(&b, "\" for header \""); - for (size_t c = 0; c < e->key_len; c++) scr_jb_putc(&b, e->key[c]); + for (size_t c = 0; c < e.key_len; c++) scr_jb_putc(&b, e.key[c]); scr_jb_putc(&b, '"'); ScrStr *msg = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE"); @@ -6226,7 +6226,7 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) { scr_arr_release(pairs); return NULL; } - scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(pairs, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(pairs, vs); } } @@ -6945,8 +6945,8 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) scr_dyn_arr_push(arr, item); } /* the list is most recent first: reverse each array into Node's order */ - for (size_t i = 0; i < obj->v.obj.len; i++) { - ScrDyn *arr = obj->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(obj); i++) { + ScrDyn *arr = scr_dyn_obj_value_at(obj, i); for (size_t l = 0, r = arr->v.arr.len ? arr->v.arr.len - 1 : 0; l < r; l++, r--) { ScrDyn *t = arr->v.arr.items[l]; arr->v.arr.items[l] = arr->v.arr.items[r]; @@ -7449,13 +7449,13 @@ bool scr_http_req_opts_read(const ScrDyn *opts, bool secure, const char *api, Sc headers = headers_view; } if (headers != NULL && headers->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - ScrStr *name = scr_str_new(e->key, e->key_len); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + ScrStr *name = scr_str_new(e.key, e.key_len); ScrStr *label = scr_str_new("", 0); scr_http_validate_header_name(name, label); scr_str_release(label); - const ScrDyn *v = e->value; + const ScrDyn *v = e.value; if (!scr_exc_pending()) { if (v->kind == SCR_DYN_ARR) { for (size_t k = 0; k < v->v.arr.len && !scr_exc_pending(); k++) diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 489946a12..30fddf8a4 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -2559,9 +2559,9 @@ ScrNetServer *scr_http2_create_secure_server_dyn(const ScrDyn *opts /*borrowed*/ ScrHttpReqFn fn) { bool allow_http1 = false; if (opts != NULL && opts->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < opts->v.obj.len; i++) { - if (strcmp(opts->v.obj.entries[i].key, "allowHTTP1") == 0) { - allow_http1 = scr_dyn_truthy(opts->v.obj.entries[i].value); + for (size_t i = 0; i < scr_dyn_obj_count(opts); i++) { + if (strcmp(scr_dyn_obj_prop_at(opts, i).key, "allowHTTP1") == 0) { + allow_http1 = scr_dyn_truthy(scr_dyn_obj_value_at(opts, i)); } } } @@ -3105,15 +3105,15 @@ static void scr_h2_dyn_fire_goaway(ScrClosure *cb, double code, double last) { /* Flatten a dyn headers object into the flat pairs array (numbers via * String(n); the :status/:method/... pseudo-headers pass through). */ static ScrArr *scr_h2_dom_to_pairs(const ScrDyn *headers) { - ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2); - for (size_t i = 0; i < headers->v.obj.len; i++) { - const ScrDynEntry *e = &headers->v.obj.entries[i]; - const ScrDyn *v = e->value; + ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, scr_dyn_obj_count(headers) * 2); + for (size_t i = 0; i < scr_dyn_obj_count(headers); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(headers, i); + const ScrDyn *v = e.value; ScrStr *vs; if (v->kind == SCR_DYN_STR) vs = scr_str_retain(v->v.str); else if (v->kind == SCR_DYN_NUM) vs = scr_f64_to_scrstr(v->v.num); else continue; /* undefined/null header: dropped, Node's stance */ - scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(pairs, scr_str_new(e.key, e.key_len)); scr_arr_push_ref(pairs, vs); } return pairs; diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index bec5be95a..e6df0f4cf 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -973,21 +973,23 @@ ScrStr *scr_insp_regex(ScrRegex *re) { * 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); + return scr_dyn_obj_count(props) + (props->symbol_keys ? props->symbol_keys->v.arr.len : 0); } -static ScrStr *insp_accessor_tag(const ScrDynEntry *ent); +static ScrStr *insp_accessor_tag(const ScrDynProp *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; + ScrDynProp found; + const ScrDynProp *ent = NULL; ScrStr *key = NULL; - if (k < props->v.obj.len) { - ent = &props->v.obj.entries[k]; + if (k < scr_dyn_obj_count(props)) { + found = scr_dyn_obj_prop_at(props, k); + ent = &found; } else { - ScrDyn *symbol = props->symbol_keys->v.arr.items[k - props->v.obj.len]; - ent = scr_dyn_own_symbol_entry(props, symbol); + ScrDyn *symbol = props->symbol_keys->v.arr.items[k - scr_dyn_obj_count(props)]; + if (scr_dyn_own_symbol_prop(props, symbol, &found)) ent = &found; if (ent && ent->enumerable) key = scr_insp_dyn(symbol, recurse, depth); } if (!ent || !ent->enumerable) return NULL; @@ -1005,10 +1007,11 @@ static ScrStr *insp_view_property(ScrDyn *props, size_t k, double recurse, doubl 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; + if (k < scr_dyn_obj_count(props)) count += scr_dyn_obj_prop_at(props, 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; + ScrDynProp ent; + count += scr_dyn_own_symbol_prop(props, props->symbol_keys->v.arr.items[k - scr_dyn_obj_count(props)], &ent) && + ent.enumerable; } } return count; @@ -1157,11 +1160,11 @@ static ScrDyn *insp_error_cause(ScrError *e, bool *enumerable, ScrDyn **view) { ScrDyn *node = scr_errdyn_view_of(e); if (node != NULL && node->kind == SCR_DYN_TYPED_REF) node = *view = scr_dyn_typed_ref_materialize(node); if (node != NULL && node->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < node->v.obj.len; i++) { - ScrDynEntry *ent = &node->v.obj.entries[i]; - if (ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0) { - *enumerable = ent->enumerable; - return ent->value; + for (size_t i = 0; i < scr_dyn_obj_count(node); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(node, i); + if (ent.key_len == 5 && memcmp(ent.key, "cause", 5) == 0) { + *enumerable = ent.enumerable; + return ent.value; } } return NULL; @@ -1176,9 +1179,9 @@ static ScrDyn *insp_error_cause(ScrError *e, bool *enumerable, ScrDyn **view) { * other non-enumerable property of an error. */ bool scr_insp_error_bag_has_properties(ScrDyn *bag) { if (bag == NULL || bag->kind != SCR_DYN_OBJ) return false; - for (size_t i = 0; i < bag->v.obj.len; i++) { - const ScrDynEntry *ent = &bag->v.obj.entries[i]; - if (ent->enumerable && !(ent->key_len == 5 && memcmp(ent->key, "cause", 5) == 0)) return true; + for (size_t i = 0; i < scr_dyn_obj_count(bag); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(bag, i); + if (ent.enumerable && !(ent.key_len == 5 && memcmp(ent.key, "cause", 5) == 0)) return true; } return false; } @@ -1282,7 +1285,7 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { * (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */ /* util.inspect's text for an accessor property: [Getter], [Setter] or * [Getter/Setter], by which of the two are functions. +1. */ -static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) { +static ScrStr *insp_accessor_tag(const ScrDynProp *ent) { bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC; bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC; const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined"; @@ -1371,12 +1374,12 @@ static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) { * Node's `{ ... }` tail. */ /* An own non-enumerable entry of the error encoding, NULL when absent or * enumerable (an enumerable one prints with the other own keys). */ -static const ScrDynEntry *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len) { - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *ent = &d->v.obj.entries[i]; - if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? NULL : ent; - } - return NULL; +static const ScrDynProp *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len, + ScrDynProp *out) { + size_t i = scr_dyn_obj_index_of(d, key, len); + if (i == SIZE_MAX) return NULL; + *out = scr_dyn_obj_prop_at(d, i); + return out->enumerable ? NULL : out; } static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { @@ -1390,19 +1393,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { memcmp(name->v.str->data, "AssertionError", 14) == 0; double value_depth = assertion ? recurse : depth; size_t extra = 0; - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; - if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + if (!ent.enumerable) continue; + if ((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)) continue; extra++; } /* formatError shows a non-enumerable `cause`, and an AggregateError's * `errors` array, after the enumerable keys: `[cause]`, `[errors]`. */ - const ScrDynEntry *cause = insp_error_hidden_entry(d, "cause", 5); - const ScrDynEntry *errors = insp_error_hidden_entry(d, "errors", 6); + ScrDynProp cause_prop, errors_prop; + const ScrDynProp *cause = insp_error_hidden_entry(d, "cause", 5, &cause_prop); + const ScrDynProp *errors = insp_error_hidden_entry(d, "errors", 6, &errors_prop); if (errors && errors->value->kind != SCR_DYN_ARR) errors = NULL; extra += (cause != NULL) + (errors != NULL); InspBuf base = {0}; @@ -1426,20 +1430,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { if (extra == 0) return base_str; scr_insp_begin(recurse + 1); scr_insp_seen_push(d); - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; - if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) || - (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || - (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + if (!ent.enumerable) continue; + if ((ent.key_len == 4 && memcmp(ent.key, "name", 4) == 0) || + (ent.key_len == 7 && memcmp(ent.key, "message", 7) == 0) || + (ent.key_len == 5 && memcmp(ent.key, "stack", 5) == 0)) continue; - bool shortened = assertion && ((ent->key_len == 6 && memcmp(ent->key, "actual", 6) == 0) || - (ent->key_len == 8 && memcmp(ent->key, "expected", 8) == 0)); - ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent->value) : scr_dyn_retain(ent->value); + bool shortened = assertion && ((ent.key_len == 6 && memcmp(ent.key, "actual", 6) == 0) || + (ent.key_len == 8 && memcmp(ent.key, "expected", 8) == 0)); + ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent.value) : scr_dyn_retain(ent.value); ScrStr *val = scr_insp_dyn(shown, recurse + 1, value_depth); scr_dyn_release(shown); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + insp_key_into(&eb, ent.key, ent.key_len); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -1447,7 +1451,7 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { scr_insp_entry(entry, false); scr_str_release(entry); } - const ScrDynEntry *hidden[] = {cause, errors}; + const ScrDynProp *hidden[] = {cause, errors}; for (size_t i = 0; i < 2; i++) { if (!hidden[i]) continue; ScrStr *val = scr_insp_dyn(hidden[i]->value, recurse + 1, value_depth); @@ -1609,7 +1613,7 @@ static bool scr_insp_is_generator(const ScrDyn *d) { } static ScrStr *scr_insp_generator(const ScrDyn *d) { - const char *tag = scr_gen_tag_reader(d->v.typed_ref.ptr); + const char *tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr); if (tag[0] == '!') { static const char message[] = "util.inspect of this iterator object is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); @@ -1690,9 +1694,9 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, /* 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 || g_show_hidden) ? ent : NULL; +static bool scr_insp_symbol_entry(ScrDyn *d, size_t i, ScrDynProp *out) { + return scr_dyn_own_symbol_prop(d, d->symbol_keys->v.arr.items[i], out) && + (out->enumerable || g_show_hidden); } /* An ordinary object: `Name { key: value }` with its constructor name @@ -1700,15 +1704,21 @@ static ScrDynEntry *scr_insp_symbol_entry(ScrDyn *d, size_t i) { * 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) { +static bool insp_obj_shows(const ScrDynProp *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 += insp_obj_shows(&d->v.obj.entries[i]); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + shown += insp_obj_shows(&ent); + } 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; + for (size_t i = 0; i < nsymbols; i++) { + ScrDynProp ent; + shown += scr_insp_symbol_entry(d, i, &ent); + } if (shown == 0) { if (cls) return scr_insp_class_tag(cls, " {}"); return d->null_proto ? scr_str_new("[Object: null prototype] {}", 27) @@ -1720,17 +1730,17 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char : 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 (!insp_obj_shows(ent)) continue; - ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(d, i); + 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}; - if (ent->enumerable) { - 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); + insp_key_into(&eb, ent.key, ent.key_len); ib_char(&eb, ']'); } ib_cstr(&eb, ": "); @@ -1742,8 +1752,9 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char if (scr_exc_pending()) break; } for (size_t i = 0; i < nsymbols; i++) { - ScrDynEntry *ent = scr_insp_symbol_entry(d, i); - if (!ent) continue; + ScrDynProp found; + if (!scr_insp_symbol_entry(d, i, &found)) continue; + const ScrDynProp *ent = &found; ScrStr *key = scr_insp_dyn(d->symbol_keys->v.arr.items[i], recurse + 1, depth); ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; @@ -2063,7 +2074,7 @@ static ScrDyn *insp_inspect_value(void) { 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_obj_set_attrs_at(table, scr_dyn_obj_count(table) - 1, true, false, true); } scr_dyn_release(table); } @@ -2100,15 +2111,15 @@ static ScrDyn *insp_user_options(double depth) { 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); + for (size_t i = 0; i < scr_dyn_obj_count(g_user_options); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(g_user_options, 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); + scr_dyn_obj_set(options, ent.key, ent.key_len, value); } } return options; @@ -2343,9 +2354,12 @@ static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { } 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; + for (size_t i = 0; i < scr_dyn_obj_count(table); i++) shown += scr_dyn_obj_prop_at(table, 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; + for (size_t i = 0; i < nsymbols; i++) { + ScrDynProp ent; + shown += scr_insp_symbol_entry(table, i, &ent); + } } if (shown == 0) { scr_dyn_release(table); @@ -2498,9 +2512,30 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * 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 += insp_obj_shows(&props->v.obj.entries[i]); - if (len == 0 && nprops == 0 && !g_show_hidden) return scr_str_new("[]", 2); - if (recurse > depth) return insp_styled_lit("[Array]", 7, INSP_SPECIAL); + for (size_t i = 0; props && i < scr_dyn_obj_count(props); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(props, i); + nprops += insp_obj_shows(&ent); + } + /* 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). */ @@ -2544,15 +2579,15 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { 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 (!insp_obj_shows(ent)) continue; - ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); + for (size_t k = 0; props && k < scr_dyn_obj_count(props); k++) { + ScrDynProp ent = scr_dyn_obj_prop_at(props, k); + 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, ']'); + 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); @@ -2570,7 +2605,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * `[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); @@ -2732,8 +2770,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(iso); ScrStr *prefix = ib_take(&head); bool fields = false; - for (size_t i = 0; i < materialized->v.obj.len && !fields; i++) - fields = materialized->v.obj.entries[i].enumerable; + for (size_t i = 0; i < scr_dyn_obj_count(materialized) && !fields; i++) + fields = scr_dyn_obj_prop_at(materialized, i).enumerable; if (!fields || recurse > depth) { scr_dyn_release(materialized); return prefix; @@ -3302,12 +3340,12 @@ bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o "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; + for (size_t i = 0; i < scr_dyn_obj_count(options) && !o->user_options; i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(options, 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; + read = ent.key_len == strlen(known[k]) && memcmp(ent.key, known[k], ent.key_len) == 0; if (!read) o->user_options = options; } } @@ -3415,10 +3453,10 @@ static ScrStr *insp_table_cell(ScrDyn *v) { } const char *base = v->null_proto ? "[Object: null prototype]" : "Object"; size_t n = 0; - for (size_t i = 0; i < v->v.obj.len; i++) { - ScrDynEntry *ent = &v->v.obj.entries[i]; - if (!ent->enumerable) continue; - if (ent->accessor) { + for (size_t i = 0; i < scr_dyn_obj_count(v); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(v, i); + if (!ent.enumerable) continue; + if (ent.accessor) { static const char message[] = "console.table cells holding accessor properties have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return scr_str_new("", 0); @@ -3442,12 +3480,12 @@ static ScrStr *insp_table_cell(ScrDyn *v) { } ScrStr *items[2]; size_t count = 0; - for (size_t i = 0; i < v->v.obj.len; i++) { - ScrDynEntry *ent = &v->v.obj.entries[i]; - if (!ent->enumerable) continue; - ScrStr *val = scr_insp_dyn(ent->value, 1, 0); + for (size_t i = 0; i < scr_dyn_obj_count(v); i++) { + ScrDynProp ent = scr_dyn_obj_prop_at(v, i); + if (!ent.enumerable) continue; + ScrStr *val = scr_insp_dyn(ent.value, 1, 0); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + insp_key_into(&eb, ent.key, ent.key_len); ib_cstr(&eb, ": "); ib_str(&eb, val); scr_str_release(val); diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 5abab2053..e3c37560d 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -180,45 +180,37 @@ ScrJsVal scr_js_increment(ScrJsVal value, bool increment); double scr_js_to_number(ScrJsVal value); bool scr_js_truthy(ScrJsVal value); -/* ── shapes and inline caches (scr_js_object.c) ───────────────────────── - * A checked object's hidden class (ScrDyn.v.obj.shape) names its key - * sequence: a node of the thread's transition tree, whose depth is the - * entry count and whose last key sits at entry index depth - 1. Shapes are - * per thread and live until the thread's runtime state is torn down; a - * thread's shape count and the key bytes they hold are bounded, and an - * object whose key sequence would need a shape past either bound stays - * unshaped, as do immortal objects, which threads share. The tier - * computes an object's shape on a cache miss; the SCR_JS_TIER build of - * scr_json.c resets it on every change to the key set. - * Attributes stay in the entries and the prototype in the node: cache hits - * test both live, so a shape only ever proves which key sits at which entry - * index and which keys an object lacks. +/* ── inline caches (scr_js_object.c) ─────────────────────────────────── + * Caches key on object layouts (scr_dyn.h): a layout-mode object's layout + * names its key sequence, and all its properties are writable data + * properties, so a layout proves which key sits at which slot and which + * keys the object lacks. Dictionary-mode objects (layout NULL), including + * immortal objects, which threads share, never hit. The prototype stays in + * the node: cache hits test it live. * * Each property site of compiled dynamic-tier code owns one zeroed, * thread-local ScrJsIc with static storage: the reset that frees a - * thread's shapes (at thread exit, and when a library session ends) clears + * thread's layouts (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 - * `holder`, the receiver's prototype, whose entry `slot` holds it. - * store entry: shape = receiver shape before the store; target == shape - * replaces entry `slot`; any other target adds the key as entry - * `slot` and moves the receiver to `target`. An add requires the - * receiver's prototype to be `holder` (NULL or an object whose - * shape is holder_shape and that lacks the key). - * keyed entry: slot only; a hit compares the key bytes stored there. */ -typedef struct ScrJsShape ScrJsShape; - + * load entry: layout = receiver layout; target = NULL for an own + * property (slot indexes the receiver's slots), else the layout of + * `holder`, the receiver's prototype, whose slot `slot` holds it. + * store entry: layout = receiver layout before the store; target == + * layout replaces slot `slot`; any other target adds the key as + * slot `slot` and moves the receiver to `target`. An add requires + * the receiver's prototype to be `holder` (NULL or an object whose + * layout is holder_layout and that lacks the key). + * keyed entry: slot only; a hit compares the key stored there. */ typedef struct ScrJsIcEntry { - const ScrJsShape *shape; - const ScrJsShape *target; + const ScrLayout *layout; + const ScrLayout *target; const ScrDyn *holder; - const ScrJsShape *holder_shape; + const ScrLayout *holder_layout; size_t slot; } ScrJsIcEntry; diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 4cee9e7e8..4d94a4e7e 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -1,19 +1,15 @@ -/* scriptc runtime — the dynamic tier's hidden classes and inline caches - * (scr_js.h; two-tier design stage S4). +/* scriptc runtime — the dynamic tier's inline caches (scr_js.h; two-tier + * design stage S4). * - * Shapes are attached to the existing checked objects (ScrDyn OBJ nodes) - * rather than replacing their entry storage: static code and the shared - * runtime read those entries in place, so a second object representation - * would have to be understood or converted at every boundary. A shape is a - * node of the thread's transition tree over key sequences; an object's - * shape proves which key sits at each entry index and which keys the - * object lacks. Everything else a cache hit depends on is tested live: the - * node kind, the accessor and writable bits of the entry it uses, and the - * prototype pointer. + * Caches key on object layouts (scr_dyn.h): a layout-mode object's layout + * proves which key sits at each slot and which keys the object lacks, and + * every property of such an object is a writable data property, so a hit + * tests the layout word and, for prototype hits, the prototype pointer. + * Dictionary-mode objects (layout NULL) never hit and take the generic + * paths. * * Linked only into programs with dynamic-tier code (the dynTier runtime - * feature), together with the SCR_JS_TIER build of scr_json.c, which - * resets an object's shape on every change to its key set. */ + * feature). */ #include "scr_js.h" #include "scr_key.h" @@ -24,61 +20,26 @@ * backend/llvm/js-ic.ts). */ #if SCR_JS_IMMEDIATES _Static_assert(offsetof(ScrDyn, kind) == 8, "kind offset"); -_Static_assert(offsetof(ScrDyn, v.obj.len) == 16 && offsetof(ScrDyn, v.obj.entries) == 32, - "object entry offsets"); +_Static_assert(offsetof(ScrDyn, v.obj.len) == 16 && offsetof(ScrDyn, v.obj.slots) == 32 && + offsetof(ScrDyn, v.obj.layout) == 80, "object slot offsets"); _Static_assert(offsetof(ScrDyn, v.arr.len) == 16 && offsetof(ScrDyn, v.arr.items) == 32 && offsetof(ScrDyn, v.arr.presence) == 56, "array element offsets"); _Static_assert(offsetof(ScrDyn, prototype) == 96, "prototype offset"); -_Static_assert(sizeof(ScrDynEntry) == 48 && offsetof(ScrDynEntry, value) == 16 && - offsetof(ScrDynEntry, writable) == 41, "entry layout"); _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layout"); #endif -/* Objects with more entries stay unshaped (dictionary mode): their reads - * take the generic path and its hash index. */ -#define SCR_JS_SHAPE_MAX 64 - -/* 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) - -struct ScrJsShape { - const ScrJsShape *parent; - char *key; /* one reference to shared key bytes (scr_json.c) */ - size_t key_len; - uint64_t hash; - uint32_t count; /* entries: the depth in the tree */ - ScrJsShape *all_next; -}; - -/* The root is the empty key sequence. Transitions live in one open - * addressing table per thread keyed by (parent, key). */ -static SCR_TL ScrJsShape scr_js_root; -static SCR_TL ScrJsShape *scr_js_all_shapes; -static SCR_TL ScrJsShape **scr_js_transitions; -static SCR_TL size_t scr_js_transition_mask; /* capacity - 1; 0 when absent */ -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. */ +/* The cache records that hold entries. Entries name layouts and prototype + * objects, so the reset of a thread's runtime state (which frees its + * layouts) 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; +static SCR_TL bool scr_js_ics_registered; /* 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. */ + * per (receiver layout, key bytes) slot, filled by the probe and checked + * like a site entry. It names layouts, so the reset clears it. */ #define SCR_JS_SHARED_LOADS 2048 typedef struct ScrJsSharedLoad { const char *key; @@ -86,7 +47,7 @@ typedef struct ScrJsSharedLoad { } ScrJsSharedLoad; static SCR_TL ScrJsSharedLoad *scr_js_shared_loads; -static void scr_js_shapes_reset(void) { +static void scr_js_ics_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); @@ -95,130 +56,33 @@ static void scr_js_shapes_reset(void) { 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; - scr_dyn_tier_key_release(shape->key, shape->key_len); - free(shape); - } - free(scr_js_transitions); - scr_js_transitions = NULL; - scr_js_transition_mask = 0; - scr_js_transition_count = 0; - scr_js_shape_key_bytes = 0; - scr_js_shapes_registered = false; + scr_js_ics_registered = false; } -static size_t scr_js_transition_slot(const ScrJsShape *parent, uint64_t hash) { - return (size_t)scr_key_mix(hash ^ (uint64_t)(uintptr_t)parent); -} - -static void scr_js_transition_insert(ScrJsShape *child) { - size_t i = scr_js_transition_slot(child->parent, child->hash) & scr_js_transition_mask; - while (scr_js_transitions[i]) i = (i + 1) & scr_js_transition_mask; - scr_js_transitions[i] = child; -} - -static void scr_js_transitions_grow(void) { - size_t old_cap = scr_js_transition_mask ? scr_js_transition_mask + 1 : 0; - size_t cap = old_cap ? old_cap * 2 : 256; - ScrJsShape **old = scr_js_transitions; - scr_js_transitions = calloc(cap, sizeof *scr_js_transitions); - if (!scr_js_transitions) scr_trap("scriptc: out of memory\n"); - scr_js_transition_mask = cap - 1; - for (size_t i = 0; i < old_cap; i++) - if (old[i]) scr_js_transition_insert(old[i]); - free(old); -} - -/* The child of `parent` for appending `key` (borrowed bytes), created on - * first use; NULL when creating it would exceed a budget. */ -static const ScrJsShape *scr_js_transition(const ScrJsShape *parent, const char *key, size_t key_len) { - uint64_t hash = scr_key_hash(key, key_len); - if (scr_js_transition_mask) { - size_t i = scr_js_transition_slot(parent, hash) & scr_js_transition_mask; - for (ScrJsShape *child; (child = scr_js_transitions[i]); i = (i + 1) & scr_js_transition_mask) { - if (child->parent == parent && child->hash == hash && child->key_len == key_len && - scr_key_equal(child->key, key, key_len)) - return child; - } - } - if (scr_js_transition_count >= SCR_JS_SHAPE_BUDGET || - key_len > SCR_JS_SHAPE_KEY_BYTES_BUDGET - scr_js_shape_key_bytes) - return NULL; - if ((scr_js_transition_count + 1) * 2 > (scr_js_transition_mask ? scr_js_transition_mask + 1 : 0)) - scr_js_transitions_grow(); - ScrJsShape *child = malloc(sizeof *child); - if (!child) scr_trap("scriptc: out of memory\n"); - *child = (ScrJsShape){parent, scr_dyn_tier_key_copy(key, key_len), key_len, hash, parent->count + 1, - scr_js_all_shapes}; - scr_js_all_shapes = child; - if (!scr_js_shapes_registered) { - scr_js_shapes_registered = true; - scr_atexit(scr_js_shapes_reset); - } - scr_js_transition_insert(child); - scr_js_transition_count++; - scr_js_shape_key_bytes += key_len; - return child; -} - -/* An object's shape, computed from its entries when it has none; NULL for - * objects that stay unshaped (other kinds, immortal objects, the builtin - * Object.prototype, dictionary-sized objects, key sequences past the - * budgets). Immortal objects (rc == SIZE_MAX: published graphs, which - * threads share by pointer) are never shaped: shapes belong to one thread - * and die with it, and no thread writes an immortal object's header. */ -#ifdef SCR_JS_VERIFY -/* Debugging aid (build the runtime with -DSCR_JS_VERIFY and compile with - * SCRIPTC_JS_IC_WAYS=0 so every access probes): traps when an object's - * shape no longer names its key sequence, i.e. when some mutation of the - * key set missed its SCR_JS_SHAPE_RESET. */ -static void scr_js_verify(const ScrDyn *object, const ScrJsShape *shape) { - size_t len = object->v.obj.len; - if (shape->count != len) scr_trap("scriptc: stale shape (entry count)\n"); - for (const ScrJsShape *s = shape; s->parent; s = s->parent) { - const ScrDynEntry *entry = &object->v.obj.entries[s->count - 1]; - if (entry->key_len != s->key_len || !scr_key_equal(entry->key, s->key, s->key_len)) - scr_trap("scriptc: stale shape (key)\n"); - } -} -#endif - -static const ScrJsShape *scr_js_shape_of(ScrDyn *object) { +/* An object's layout, or NULL for objects caches leave alone: other kinds, + * dictionary-mode objects (immortal objects, which threads share, are + * always in dictionary mode), and the builtin Object.prototype. */ +static inline const ScrLayout *scr_js_layout_of(const ScrDyn *object) { if (object->kind != SCR_DYN_OBJ || object->rc == SIZE_MAX) return NULL; - const ScrJsShape *shape = object->v.obj.shape; - if (shape == &scr_js_unshaped) return NULL; -#ifdef SCR_JS_VERIFY - if (shape) scr_js_verify(object, shape); -#endif - if (shape) return shape; - size_t len = object->v.obj.len; - if (len > SCR_JS_SHAPE_MAX || object == scr_dyn_tier_object_prototype()) return NULL; - shape = &scr_js_root; - for (size_t i = 0; i < len; i++) { - ScrDynEntry *entry = &object->v.obj.entries[i]; - shape = scr_js_transition(shape, entry->key, entry->key_len); - if (!shape) { - object->v.obj.shape = &scr_js_unshaped; - return NULL; - } - } - object->v.obj.shape = (ScrJsShape *)shape; - return shape; + const ScrLayout *layout = object->v.obj.layout; + if (!layout || object == scr_dyn_tier_object_prototype()) return NULL; + return layout; } -/* The entry index of `key` in an object of shape `shape`, or SIZE_MAX. */ -static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t key_len) { - for (; shape->parent; shape = shape->parent) - if (shape->key_len == key_len && scr_key_equal(shape->key, key, key_len)) return shape->count - 1; - return SIZE_MAX; +/* The slot of `key` in an object of layout `layout`, or SIZE_MAX. */ +static size_t scr_js_layout_find(const ScrLayout *layout, const char *key, size_t key_len) { + uint32_t slot = scr_layout_find(layout, key, key_len); + return slot == UINT32_MAX ? SIZE_MAX : slot; } /* 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_ics_registered) { + scr_js_ics_registered = true; + scr_atexit(scr_js_ics_reset); + } 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); @@ -243,26 +107,25 @@ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { /* ── loads ────────────────────────────────────────────────────────────── */ -/* The cached answer of an entry whose receiver shape matched, or NULL. */ +/* The cached answer of an entry whose receiver layout matched, or NULL. */ static ScrDyn *scr_js_load_hit(ScrDyn *receiver, const ScrJsIcEntry *entry) { const ScrDyn *holder = receiver; if (entry->target) { holder = entry->holder; if (receiver->prototype != holder || holder->kind != SCR_DYN_OBJ || - holder->v.obj.shape != entry->target) + holder->v.obj.layout != entry->target) return NULL; } - const ScrDynEntry *found = &holder->v.obj.entries[entry->slot]; - return found->accessor ? NULL : scr_dyn_retain(found->value); + return scr_dyn_retain(holder->v.obj.slots[entry->slot]); } /* 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; + * sends such a receiver to the probe. Its holder_layout and slot name the + * one prototype the read walked past (layout and object), or NULL and 0 when + * the receiver had no prototype. */ +static const ScrLayout 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 @@ -288,7 +151,7 @@ static bool scr_js_absent_cacheable(const ScrStr *key) { return true; } -static ScrJsSharedLoad *scr_js_shared_load(const ScrJsShape *shape, const ScrStr *key) { +static ScrJsSharedLoad *scr_js_shared_load(const ScrLayout *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"); @@ -305,7 +168,7 @@ static bool scr_js_absent_hit(const ScrDyn *receiver, const ScrJsIcEntry *entry) 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; + walked->v.obj.layout == entry->holder_layout && walked->prototype == NULL; } /* The answer of a matching entry for `receiver`, or NULL when the entry no @@ -344,11 +207,11 @@ static void scr_js_load_fill(ScrJsIc *ic, uint32_t stale, ScrJsSharedLoad *share * 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); + const ScrLayout *shape = scr_js_layout_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; + if (ic->entry[i].layout != shape) continue; ScrDyn *value = scr_js_entry_answer(receiver, &ic->entry[i]); if (value) return value; if (stale == UINT32_MAX) stale = i; @@ -356,18 +219,16 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con 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 && + if (shared->key == key->data && shared->entry.layout == 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); + size_t slot = scr_js_layout_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_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, NULL, NULL, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(receiver->v.obj.slots[slot]); } absent = absent && scr_js_absent_cacheable(key); /* One prototype link: an ordinary object other than the builtin @@ -379,9 +240,9 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con (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); + const ScrLayout *holder_shape = scr_js_layout_of(holder); if (!holder_shape) return NULL; - slot = scr_js_shape_find(holder_shape, key->data, key->len); + slot = scr_js_layout_find(holder_shape, key->data, key->len); if (slot == SIZE_MAX) { if (!absent || holder->prototype != NULL) return NULL; scr_js_load_fill(ic, stale, shared, key, @@ -389,13 +250,11 @@ static __attribute__((noinline)) ScrDyn *scr_js_probe_load(ScrDyn *receiver, con (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_load_fill(ic, stale, shared, key, (ScrJsIcEntry){shape, holder_shape, holder, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(holder->v.obj.slots[slot]); } -/* Receivers other than ordinary objects have no shape: they miss before +/* Receivers other than ordinary objects have no layout: 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; @@ -427,62 +286,56 @@ 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 || found->fenced) return false; scr_dyn_region_store(receiver, value); - ScrDyn *old = found->value; - found->value = scr_dyn_retain(value); + ScrDyn **found = &receiver->v.obj.slots[slot]; + ScrDyn *old = *found; + *found = scr_dyn_retain(value); scr_dyn_release(old); return true; } /* Whether an add may skip the generic path's prototype-chain walk: the * chain is empty, or one ordinary object that lacks the key and ends it. */ -static bool scr_js_add_chain(const ScrDyn *prototype, const ScrJsShape *prototype_shape, +static bool scr_js_add_chain(const ScrDyn *prototype, const ScrLayout *prototype_shape, const ScrStr *key) { if (!prototype) return true; return prototype_shape && !prototype->prototype && - scr_js_shape_find(prototype_shape, key->data, key->len) == SIZE_MAX; + scr_js_layout_find(prototype_shape, key->data, key->len) == SIZE_MAX; } -static bool scr_js_append_shaped(ScrDyn *receiver, const ScrJsShape *target, ScrDyn *value) { +static bool scr_js_append_shaped(ScrDyn *receiver, const ScrLayout *target, ScrDyn *value) { if (receiver->non_extensible) return false; - scr_dyn_tier_append(receiver, target->key, target->key_len, scr_dyn_retain(value)); - receiver->v.obj.shape = (ScrJsShape *)target; + scr_dyn_tier_append(receiver, target, scr_dyn_retain(value)); return true; } bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJsIc *ic) { - const ScrJsShape *shape = scr_js_shape_of(receiver); + const ScrLayout *shape = scr_js_layout_of(receiver); if (!shape || scr_js_error_synced_key(key)) return false; uint32_t stale = UINT32_MAX; for (uint32_t i = 0; i < ic->count; i++) { const ScrJsIcEntry *entry = &ic->entry[i]; - if (entry->shape != shape) continue; + if (entry->layout != shape) continue; if (entry->target == shape) { if (scr_js_replace(receiver, entry->slot, value)) return true; } else if (receiver->prototype == entry->holder && (!entry->holder || (entry->holder->kind == SCR_DYN_OBJ && - entry->holder->v.obj.shape == entry->holder_shape && + entry->holder->v.obj.layout == entry->holder_layout && !entry->holder->prototype))) { if (scr_js_append_shaped(receiver, entry->target, value)) return true; } stale = i; break; } - size_t slot = scr_js_shape_find(shape, key->data, key->len); + size_t slot = scr_js_layout_find(shape, key->data, key->len); if (slot != SIZE_MAX) { - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - 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); } ScrDyn *prototype = receiver->prototype; - const ScrJsShape *prototype_shape = prototype ? scr_js_shape_of(prototype) : NULL; - if (!scr_js_add_chain(prototype, prototype_shape, key) || receiver->non_extensible || - shape->count >= SCR_JS_SHAPE_MAX) - return false; - const ScrJsShape *target = scr_js_transition(shape, key->data, key->len); + const ScrLayout *prototype_shape = prototype ? scr_js_layout_of(prototype) : NULL; + if (!scr_js_add_chain(prototype, prototype_shape, key) || receiver->non_extensible) return false; + const ScrLayout *target = scr_layout_add(shape, key->data, key->len); if (!target) return false; scr_js_ic_add(ic, stale, (ScrJsIcEntry){shape, target, prototype, prototype_shape, target->count - 1}); return scr_js_append_shaped(receiver, target, value); @@ -505,20 +358,18 @@ ScrDyn *scr_js_keyed_probe(ScrDyn *receiver, const ScrDyn *key, ScrJsIc *ic) { if (key->kind == SCR_DYN_NUM) return scr_js_index_probe(receiver, key->v.num); if (key->kind != SCR_DYN_STR || receiver->kind != SCR_DYN_OBJ) return NULL; const ScrStr *name = key->v.str; + const ScrLayout *shape = scr_js_layout_of(receiver); + if (!shape) return NULL; size_t len = receiver->v.obj.len; for (uint32_t i = 0; i < ic->count; i++) { size_t slot = ic->entry[i].slot; if (slot >= len) continue; - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->key_len == name->len && scr_key_equal(found->key, name->data, name->len)) - return found->accessor ? NULL : scr_dyn_retain(found->value); + const ScrLayoutKey *found = &shape->keys[slot]; + if (found->len == name->len && scr_key_equal(found->key, name->data, name->len)) + return scr_dyn_retain(receiver->v.obj.slots[slot]); } - const ScrJsShape *shape = scr_js_shape_of(receiver); - if (!shape) return NULL; - size_t slot = scr_js_shape_find(shape, name->data, name->len); + size_t slot = scr_js_layout_find(shape, name->data, name->len); if (slot == SIZE_MAX) return NULL; - const ScrDynEntry *found = &receiver->v.obj.entries[slot]; - if (found->accessor) return NULL; scr_js_ic_add(ic, UINT32_MAX, (ScrJsIcEntry){NULL, NULL, NULL, NULL, slot}); - return scr_dyn_retain(found->value); + return scr_dyn_retain(receiver->v.obj.slots[slot]); } diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index 06b804759..02368ca74 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -5,7 +5,7 @@ #include #include -/* Mirrors scr_js_object.c's budgets. */ +/* Mirrors scr_json.c's layout budgets. */ #define SHAPE_BUDGET ((size_t)1 << 16) #define KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -63,7 +63,8 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { scr_str_release(key); scr_dyn_release(object); } - assert(shaped == budget); + /* Objects the earlier checks built hold a few layouts already. */ + assert(shaped <= budget && shaped + 64 > budget); /* A key sequence shaped before the budget ran out. */ ScrStr *first = key_of(bytes, key_len, 0); @@ -73,7 +74,7 @@ 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. */ memset(&store_ic, 0, sizeof store_ic); - ScrStr *added = scr_str_new("added", 5); + ScrStr *added = key_of(bytes, key_len, budget + 1000); ScrDyn *value = scr_dyn_new_num(8); assert(!scr_js_store_probe(object, added, value, &store_ic)); assert(object->v.obj.len == 1); @@ -94,11 +95,11 @@ static void immortal_objects_stay_unshaped(void) { ScrDyn *object = object_with(key, 3); object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); - assert(object->v.obj.shape == NULL); + assert(object->v.obj.len == 1); memset(&store_ic, 0, sizeof store_ic); ScrDyn *value = scr_dyn_new_num(4); assert(!scr_js_store_probe(object, key, value, &store_ic)); - assert(object->v.obj.shape == NULL); + assert(object->v.obj.len == 1); scr_dyn_release(value); scr_str_release(key); } diff --git a/packages/runtime/src/scr_js_object.test.ts b/packages/runtime/src/scr_js_object.test.ts index 1fb90cdaa..505bd4062 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, and absent reads are cached", async () => { +test("layouts stay within the layout and key-byte budgets, and dynamic-tier 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"); @@ -29,6 +29,7 @@ test("dynamic-tier shapes stay within the shape and key-byte budgets, and absent "scr_json.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 4c8cce6b9..eb40100cf 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -24,16 +24,6 @@ #include "scr_runtime.h" #include "scr_key.h" -/* Programs with dynamic-tier code link the SCR_JS_TIER build of this unit: - * its object nodes carry the tier's hidden class (ScrDyn.v.obj.shape), and - * every allocation and change to an object's key set resets it so the - * tier recomputes it on its next cache miss. Other programs link the - * ordinary build, where this expands to nothing. */ -#ifdef SCR_JS_TIER -#define SCR_JS_SHAPE_RESET(d) ((d)->v.obj.shape = NULL) -#else -#define SCR_JS_SHAPE_RESET(d) ((void)0) -#endif static SCR_TL ScrDyn *scr_builtin_object_prototype; @@ -842,7 +832,7 @@ static size_t scr_dyn_key_slot(const char *key, size_t length) { return hash % SCR_DYN_KEY_SHARE_SLOTS; } -static char *scr_dyn_key_copy(const char *key, size_t length) { +static inline __attribute__((always_inline)) char *scr_dyn_key_copy(const char *key, size_t length) { ScrDynKeySlot *slot = length <= SCR_DYN_KEY_SHARE_LENGTH ? &scr_dyn_keys[scr_dyn_key_slot(key, length)] : NULL; if (slot && slot->key && slot->length == length && @@ -877,6 +867,246 @@ static void scr_dyn_key_release(char *key, size_t length) { free(owned); } +static inline void scr_dyn_key_retain(char *key) { + ((ScrDynKey *)(key - offsetof(ScrDynKey, bytes)))->rc++; +} + +/* ── layouts (scr_dyn.h) ─────────────────────────────────────────────── + * The thread's transition tree: the root is the empty key sequence, and a + * child appends one key to its parent's sequence. Transitions live in one + * open addressing table keyed by (parent, key); each parent also remembers + * its most recent child, which is all a loop building records of one + * shape ever needs. A chain of layouts shares one key array: a child + * appends its key to its parent's array when nothing was appended past the + * parent yet, and copies the parent's keys into a new array otherwise. + * + * A thread creates at most SCR_LAYOUT_BUDGET layouts holding at most + * SCR_LAYOUT_KEY_BYTES_BUDGET key bytes (and key arrays of at most + * SCR_LAYOUT_ARRAY_BYTES_BUDGET); past either, objects whose key sequence + * has no layout yet move to dictionary mode, and layouts that exist keep + * serving. Everything is freed with the thread's runtime state (at thread + * exit and when a library session ends). */ +#define SCR_LAYOUT_BUDGET ((size_t)1 << 16) +#define SCR_LAYOUT_KEY_BYTES_BUDGET ((size_t)4 << 20) +#define SCR_LAYOUT_ARRAY_BYTES_BUDGET ((size_t)16 << 20) +/* Layouts with more keys than this find keys through a hash index. */ +#define SCR_LAYOUT_LINEAR_MAX 8 + +typedef struct ScrLayoutKeys { + uint32_t used; + uint32_t cap; + ScrLayoutKey items[]; +} ScrLayoutKeys; + +static SCR_TL ScrLayout scr_layout_empty; +static SCR_TL ScrLayout *scr_layout_all; +static SCR_TL ScrLayout **scr_layout_transitions; +static SCR_TL size_t scr_layout_transition_mask; /* capacity - 1; 0 when absent */ +static SCR_TL size_t scr_layout_count; +static SCR_TL size_t scr_layout_key_bytes; +static SCR_TL size_t scr_layout_array_bytes; +static SCR_TL bool scr_layout_registered; + +const ScrLayout *scr_layout_root(void) { return &scr_layout_empty; } + +static void scr_layout_reset(void) { + while (scr_layout_all) { + ScrLayout *layout = scr_layout_all; + scr_layout_all = layout->all_next; + const ScrLayoutKey *key = &layout->keys[layout->count - 1]; + scr_dyn_key_release(key->key, key->len); + /* Layouts sharing a key array are newer than the one that owns it. */ + if (layout->owns_store) free(layout->store); + free(layout->lookup); + free(layout); + } + free(scr_layout_empty.lookup); + scr_layout_empty = (ScrLayout){0}; + free(scr_layout_transitions); + scr_layout_transitions = NULL; + scr_layout_transition_mask = 0; + scr_layout_count = 0; + scr_layout_key_bytes = 0; + scr_layout_array_bytes = 0; + scr_layout_registered = false; +} + +#ifdef SCR_RC_AUDIT +/* For the runtime's unit tests: frees the layouts while no object uses one. */ +void scr_layout_reset_idle(void) { scr_layout_reset(); } +#endif + +static size_t scr_layout_slot(const ScrLayout *parent, uint32_t hash) { + return (size_t)scr_key_mix((uint64_t)hash ^ (uint64_t)(uintptr_t)parent); +} + +static void scr_layout_insert(ScrLayout *child) { + size_t i = scr_layout_slot(child->parent, child->keys[child->count - 1].hash) & scr_layout_transition_mask; + while (scr_layout_transitions[i]) i = (i + 1) & scr_layout_transition_mask; + scr_layout_transitions[i] = child; +} + +static void scr_layout_transitions_grow(void) { + size_t old_cap = scr_layout_transition_mask ? scr_layout_transition_mask + 1 : 0; + size_t cap = old_cap ? old_cap * 2 : 256; + ScrLayout **old = scr_layout_transitions; + scr_layout_transitions = calloc(cap, sizeof *scr_layout_transitions); + if (!scr_layout_transitions) scr_json_oom(); + scr_layout_transition_mask = cap - 1; + for (size_t i = 0; i < old_cap; i++) + if (old[i]) scr_layout_insert(old[i]); + free(old); +} + +static inline bool scr_layout_key_is(const ScrLayoutKey *k, const char *key, size_t key_len) { + return k->len == key_len && (k->key == key || scr_key_equal(k->key, key, key_len)); +} + +static __attribute__((noinline)) void scr_layout_index(ScrLayout *layout) { + size_t cap = 16; + while (cap < 2 * (size_t)layout->count) cap *= 2; + uint8_t *table = calloc(cap, 1); + if (!table) scr_json_oom(); + for (uint32_t i = 0; i < layout->count; i++) { + size_t slot = layout->keys[i].hash & (cap - 1); + while (table[slot]) slot = (slot + 1) & (cap - 1); + table[slot] = (uint8_t)(i + 1); + } + layout->lookup = table; + layout->lookup_mask = (uint32_t)(cap - 1); +} + +uint32_t scr_layout_find(const ScrLayout *layout, const char *key, size_t key_len) { + uint32_t count = layout->count; + if (count <= SCR_LAYOUT_LINEAR_MAX) { + for (uint32_t i = 0; i < count; i++) + if (scr_layout_key_is(&layout->keys[i], key, key_len)) return i; + return UINT32_MAX; + } + if (!layout->lookup) scr_layout_index((ScrLayout *)layout); + uint32_t hash = (uint32_t)scr_key_hash(key, key_len); + size_t mask = layout->lookup_mask; + for (size_t slot = hash & mask;; slot = (slot + 1) & mask) { + uint8_t found = layout->lookup[slot]; + if (!found) return UINT32_MAX; + const ScrLayoutKey *k = &layout->keys[found - 1]; + if (k->hash == hash && scr_layout_key_is(k, key, key_len)) return found - 1u; + } +} + +const ScrLayout *scr_layout_add(const ScrLayout *layout, const char *key, size_t key_len) { + ScrLayout *parent = (ScrLayout *)layout; + ScrLayout *last = parent->last_child; + if (last && scr_layout_key_is(&last->keys[last->count - 1], key, key_len)) return last; + if (parent->count >= SCR_LAYOUT_MAX_KEYS || key_len > UINT32_MAX) return NULL; + uint32_t hash = (uint32_t)scr_key_hash(key, key_len); + if (scr_layout_transition_mask) { + size_t i = scr_layout_slot(parent, hash) & scr_layout_transition_mask; + for (ScrLayout *child; (child = scr_layout_transitions[i]); i = (i + 1) & scr_layout_transition_mask) { + const ScrLayoutKey *k = &child->keys[child->count - 1]; + if (child->parent == parent && k->hash == hash && scr_layout_key_is(k, key, key_len)) { + parent->last_child = child; + return child; + } + } + } + if (scr_layout_count >= SCR_LAYOUT_BUDGET || + key_len > SCR_LAYOUT_KEY_BYTES_BUDGET - scr_layout_key_bytes) + return NULL; + uint32_t count = parent->count + 1; + ScrLayoutKeys *store = parent->store; + bool owns = !store || store->used != parent->count || store->used == store->cap; + if (owns) { + uint32_t cap = count < 4 ? 4 : count * 2; + if (cap > SCR_LAYOUT_MAX_KEYS) cap = SCR_LAYOUT_MAX_KEYS; + size_t bytes = sizeof *store + cap * sizeof(ScrLayoutKey); + if (bytes > SCR_LAYOUT_ARRAY_BYTES_BUDGET - scr_layout_array_bytes) return NULL; + store = malloc(bytes); + if (!store) scr_json_oom(); + if (parent->count) memcpy(store->items, parent->keys, parent->count * sizeof(ScrLayoutKey)); + store->used = parent->count; + store->cap = cap; + scr_layout_array_bytes += bytes; + } + if ((scr_layout_count + 1) * 2 > (scr_layout_transition_mask ? scr_layout_transition_mask + 1 : 0)) + scr_layout_transitions_grow(); + ScrLayout *child = malloc(sizeof *child); + if (!child) scr_json_oom(); + store->items[store->used++] = (ScrLayoutKey){scr_dyn_key_copy(key, key_len), (uint32_t)key_len, hash}; + *child = (ScrLayout){store->items, count, 0, NULL, parent, NULL, store, scr_layout_all, owns}; + scr_layout_all = child; + if (!scr_layout_registered) { + scr_layout_registered = true; + scr_atexit(scr_layout_reset); + } + scr_layout_insert(child); + scr_layout_count++; + scr_layout_key_bytes += key_len; + parent->last_child = child; + return child; +} + +#define SCR_DYN_OBJECT_INDEX_MIN 32 + +/* Layout mode → dictionary mode: the same properties in the same order, + * each a writable, enumerable, configurable data property. */ +void scr_dyn_to_dictionary(ScrDyn *object) { + const ScrLayout *layout = object->v.obj.layout; + if (!layout) return; + size_t len = object->v.obj.len; + ScrDyn **slots = object->v.obj.slots; + size_t cap = len < 4 ? 4 : len; + ScrDynEntry *entries = scr_mem_realloc(NULL, cap * sizeof *entries); + if (!entries) scr_json_oom(); + memset(entries, 0, cap * sizeof *entries); + size_t traced = object->v.obj.slots_traced; + for (size_t i = 0; i < len; i++) { + const ScrLayoutKey *k = &layout->keys[i]; + scr_dyn_key_retain(k->key); + ScrDynEntry *e = &entries[i]; + e->key = k->key; + e->key_len = k->len; + e->value = slots[i]; + e->value_traced = (traced >> i) & 1; + e->writable = e->enumerable = e->configurable = true; + } + scr_mem_free(slots); + object->v.obj.entries = entries; + object->v.obj.cap = cap; + object->v.obj.layout = NULL; + object->v.obj.slots_traced = 0; + /* A wide object gets its hash index with its next insertion + * (scr_dyn_obj_put); until then lookups scan its entries. */ + object->v.obj.index = NULL; +} + +/* Appends a property `key` does not name yet to a layout-mode object, + * taking ownership of `value`; false (nothing done) when no layout fits, + * and the caller then takes the dictionary path. */ +/* Appends `value` (owned) to a layout-mode object as the slot of the last + * key of `next`, a child of the object's layout. */ +static inline void scr_dyn_obj_layout_push(ScrDyn *obj, const ScrLayout *next, ScrDyn *value) { + size_t len = obj->v.obj.len; + if (len == obj->v.obj.cap) { + size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; + if (cap > SCR_LAYOUT_MAX_KEYS) cap = SCR_LAYOUT_MAX_KEYS; + ScrDyn **slots = scr_mem_realloc(obj->v.obj.slots, cap * sizeof *slots); + if (!slots) scr_json_oom(); + obj->v.obj.slots = slots; + obj->v.obj.cap = cap; + } + obj->v.obj.slots[len] = value; + obj->v.obj.len = len + 1; + obj->v.obj.layout = next; +} + +static inline bool scr_dyn_obj_layout_append(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + const ScrLayout *next = scr_layout_add(obj->v.obj.layout, key, key_len); + if (!next) return false; + scr_dyn_obj_layout_push(obj, next, value); + return true; +} + #ifndef SCR_RC_AUDIT static SCR_TL ScrDyn *scr_dyn_free_arr, *scr_dyn_free_obj, *scr_dyn_free_misc; static SCR_TL size_t scr_dyn_free_count; @@ -887,39 +1117,25 @@ static SCR_TL size_t scr_dyn_free_count; * arena never returns them to the system either), at the cost of reserving * them, with their item/entry buffers, for dyn nodes. */ #define SCR_DYN_FREE_MAX ((size_t)1 << 20) -/* Recycled small objects may retain key bytes, but never values or an - * unbounded document's keys. This budget applies across the whole pool. */ -static SCR_TL size_t scr_dyn_cached_key_bytes; -#define SCR_DYN_KEY_CACHE_MAX ((size_t)64 * 1024) -#define SCR_DYN_KEY_CACHE_SLOTS 16 -#define SCR_DYN_KEY_CACHE_LENGTH 64 #endif -/* 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 && - bytes <= SCR_DYN_KEY_CACHE_MAX - scr_dyn_cached_key_bytes) { - scr_dyn_cached_key_bytes += bytes; - kept += bytes; - continue; +/* Drops a disposed object's keys and storage: a dictionary's entries with the keys + * they hold (recycled entries past len may hold keys too), or a layout-mode + * object's slots unless `keep_slots` (a pooled node keeps them for its next + * life). Leaves the node empty, in layout mode's representation. */ +static void scr_dyn_obj_drop_keys(ScrDyn *d, bool keep_slots) { + if (d->v.obj.layout) { + if (keep_slots) return; + scr_mem_free(d->v.obj.slots); + } else { + for (size_t i = 0; i < d->v.obj.cap; i++) { + ScrDynEntry *entry = &d->v.obj.entries[i]; + if (entry->key) scr_dyn_key_release(entry->key, entry->key_len); } -#else - (void)cache; -#endif - scr_dyn_key_release(entry->key, entry->key_len); - entry->key = NULL; - entry->key_len = 0; + scr_mem_free(d->v.obj.entries); } - return kept; + d->v.obj.slots = NULL; + d->v.obj.cap = 0; } /* Primitive kinds cannot acquire properties or change kind while live, so @@ -948,6 +1164,17 @@ void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) { if (d->v.arr.properties) visit(d->v.arr.properties, ctx); break; case SCR_DYN_OBJ: + if (d->v.obj.layout) { + size_t traced = 0; + for (size_t i = 0; i < d->v.obj.len; i++) { + ScrDyn *value = d->v.obj.slots[i]; + if (!scr_dyn_member_is_traced(value)) continue; + traced |= (size_t)1 << i; + visit(value, ctx); + } + d->v.obj.slots_traced = traced; + break; + } for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *entry = &d->v.obj.entries[i]; entry->value_traced = scr_dyn_member_is_traced(entry->value); @@ -1120,12 +1347,11 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { d->v.arr.presence = NULL; 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.len = 0; /* cap/slots preserved */ d->v.obj.region = NULL; d->v.obj.class_name = NULL; /* a released class prototype's name */ - SCR_JS_SHAPE_RESET(d); + d->v.obj.layout = &scr_layout_empty; + d->v.obj.slots_traced = 0; d->v.obj.source_identity = NULL; d->v.obj.source_access = NULL; } else if (kind != SCR_DYN_NUM && kind != SCR_DYN_STR) { @@ -1144,9 +1370,10 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) { fresh->prototype = NULL; fresh->symbol_properties = NULL; fresh->symbol_keys = NULL; -#ifdef SCR_JS_TIER - if (kind == SCR_DYN_OBJ) SCR_JS_SHAPE_RESET(fresh); -#endif + if (kind == SCR_DYN_OBJ) { + memset(&fresh->v, 0, sizeof fresh->v); + fresh->v.obj.layout = &scr_layout_empty; + } #ifdef SCR_RC_AUDIT scr_live_dyns++; #endif @@ -1225,7 +1452,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { return; } #endif - if (d->kind == SCR_DYN_OBJ) { + if (d->kind == SCR_DYN_OBJ && !d->v.obj.layout) { free(d->v.obj.index); d->v.obj.index = NULL; } @@ -1259,7 +1486,12 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { } break; case SCR_DYN_OBJ: - for (size_t i = 0; i < d->v.obj.len; i++) { + if (d->v.obj.layout) { + size_t traced = collected ? d->v.obj.slots_traced : 0; + for (size_t i = 0; i < d->v.obj.len; i++) + if (!((traced >> i) & 1)) scr_dyn_release_child(d->v.obj.slots[i]); + d->v.obj.len = 0; + } else 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_child(value); if (!collected) { @@ -1313,7 +1545,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) d->v.obj.cached_key_bytes = scr_dyn_obj_drop_keys(d, true); + if (d->kind == SCR_DYN_OBJ) 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); @@ -1327,10 +1559,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) { } #endif if (d->kind == SCR_DYN_ARR) scr_mem_free(d->v.arr.items); - else if (d->kind == SCR_DYN_OBJ) { - scr_dyn_obj_drop_keys(d, false); - scr_mem_free(d->v.obj.entries); - } + else if (d->kind == SCR_DYN_OBJ) scr_dyn_obj_drop_keys(d, false); scr_cyc_free_inline(d); } @@ -1412,7 +1641,6 @@ void scr_dyn_release(ScrDyn *d) { /* Small objects retain their compact linear representation. A wide object * owns a bucket index of entry positions; entries remain insertion ordered. * No key/value ownership lives here, and callbacks never retain its slots. */ -#define SCR_DYN_OBJECT_INDEX_MIN 32 typedef struct ScrDynObjectIndex { size_t buckets; size_t slots[]; /* zero is empty; occupied slots store entry + 1 */ @@ -1472,8 +1700,12 @@ static __attribute__((noinline)) ScrDynEntry *scr_dyn_find_indexed_entry( return NULL; } +/* The entry of own property `key`. Its callers read or write entries, so + * a layout-mode object moves to dictionary mode first; paths that know + * layouts look up slots with scr_dyn_layout_slot instead. */ static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, size_t length) { if (object->kind != SCR_DYN_OBJ) return NULL; + if (object->v.obj.layout) scr_dyn_to_dictionary((ScrDyn *)object); if (object->v.obj.index) return scr_dyn_find_indexed_entry(object, key, length); size_t slot = (length + (length ? (unsigned char)key[0] * 7u : 0)) % SCR_DYN_PROPERTY_HINTS; size_t hint = scr_dyn_property_hints[slot]; @@ -1486,16 +1718,49 @@ static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, si return entry; } +/* Out-of-line property accessors for emitted object walks (the + * index-signature checks of backend/llvm/dyn.ts). */ +const char *scr_dyn_member_key(const ScrDyn *object, size_t i) { + size_t key_len; + return scr_dyn_obj_key_at(object, i, &key_len); +} + +size_t scr_dyn_member_key_len(const ScrDyn *object, size_t i) { + size_t key_len; + scr_dyn_obj_key_at(object, i, &key_len); + return key_len; +} + +ScrDyn *scr_dyn_member_value(const ScrDyn *object, size_t i) { + return scr_dyn_obj_value_at(object, i); +} + +/* The slot of own property `key` of a layout-mode object, or NULL. */ +static inline ScrDyn **scr_dyn_layout_slot(const ScrDyn *object, const char *key, size_t length) { + uint32_t i = scr_layout_find(object->v.obj.layout, key, length); + return i == UINT32_MAX ? NULL : &object->v.obj.slots[i]; +} + +/* Whether `object` is an ordinary object in layout mode. */ +static inline bool scr_dyn_is_layout(const ScrDyn *object) { + return object->kind == SCR_DYN_OBJ && object->v.obj.layout; +} + ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len) { if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("raw object inspection"); return NULL; } + if (scr_dyn_is_layout(d)) { + ScrDyn **slot = scr_dyn_layout_slot(d, key, key_len); + return slot ? *slot : NULL; + } ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len); return entry ? entry->value : NULL; } bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len) { + if (scr_dyn_is_layout(d)) return scr_dyn_layout_slot(d, key, key_len) != NULL; ScrDynEntry *entry = scr_dyn_find_entry(d, key, key_len); return entry && entry->enumerable; } @@ -1539,8 +1804,18 @@ int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *co if (!d) return -1; if (d->kind == SCR_DYN_TYPED_REF) return -2; if (d->kind != SCR_DYN_OBJ) return -1; + if (d->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(d, key->data, key->len); + if (!slot) return -2; + const ScrDyn *member = *slot; + 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; + } for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *entry = &d->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(d, i); + const ScrDynProp *entry = &entry_prop; 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; @@ -1868,6 +2143,17 @@ ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) { * A recycled entry can reuse equal key bytes without a malloc/free pair. */ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { scr_dyn_region_store(obj, value); + if (obj->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(obj, key, key_len); + if (slot) { + ScrDyn *old = *slot; + *slot = value; + scr_dyn_release(old); + return; + } + if (!obj->non_extensible && scr_dyn_obj_layout_append(obj, key, key_len, value)) return; + scr_dyn_to_dictionary(obj); + } /* Construction usually appends an absent name. Avoid a read hint that * repeats comparisons on compact objects; wide objects use their index. */ ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) @@ -1901,14 +2187,13 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn } if (obj->v.obj.len == obj->v.obj.cap) { size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; - ScrDynEntry *entries = scr_mem_realloc(obj->v.obj.entries, cap * sizeof *entries); + ScrDynEntry *entries = scr_mem_realloc(scr_dyn_obj_dict_entries(obj), cap * sizeof *entries); if (!entries) scr_json_oom(); memset(entries + obj->v.obj.cap, 0, (cap - obj->v.obj.cap) * sizeof *entries); obj->v.obj.entries = entries; obj->v.obj.cap = cap; } - SCR_JS_SHAPE_RESET(obj); - ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++]; + ScrDynEntry *e = &scr_dyn_obj_dict_entries(obj)[obj->v.obj.len++]; if (!e->key || e->key_len != key_len || !scr_key_equal(e->key, key, key_len)) { char *owned = scr_dyn_key_copy(key, key_len); scr_dyn_key_release(e->key, e->key_len); @@ -2550,7 +2835,7 @@ 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]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(view)[i]; entry->configurable = false; if (!entry->accessor) entry->writable = false; } @@ -2614,7 +2899,8 @@ double scr_dyn_class_view_integrity(const ScrDyn *view, double bits, ScrDyn *bag 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]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(table, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable) sealed = false; if (!entry->accessor && entry->writable) frozen = false; } @@ -2795,7 +3081,7 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) { } if (object->kind == SCR_DYN_OBJ) { object->non_extensible = true; - for (size_t i = 0; i < object->v.obj.len; i++) object->v.obj.entries[i].configurable = false; + for (size_t i = 0; i < object->v.obj.len; i++) scr_dyn_obj_dict_entries(object)[i].configurable = false; } if (object->kind == SCR_DYN_ARR) { object->non_extensible = true; @@ -2826,7 +3112,7 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { 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; + for (size_t i = 0; i < object->v.obj.len; i++) if (scr_dyn_obj_prop_at(object, i).configurable) return false; return true; } @@ -2855,6 +3141,10 @@ static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *ke return owner; } if (current->kind != SCR_DYN_OBJ) continue; + if (current->v.obj.layout) { + if (scr_layout_find(current->v.obj.layout, key, length) != UINT32_MAX) return current; + continue; + } for (size_t i = 0; i < current->v.obj.len; i++) { const ScrDynEntry *entry = ¤t->v.obj.entries[i]; if (entry->key_len == length && memcmp(entry->key, key, length) == 0) return current; @@ -2897,7 +3187,7 @@ ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); bool plain = view && view->kind == SCR_DYN_OBJ; if (plain) for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(view)[i]; if (entry->accessor || !entry->configurable || !entry->writable) plain = false; } scr_dyn_release(view); @@ -3267,6 +3557,16 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) { if (value->kind == SCR_DYN_BOOL || value->kind == SCR_DYN_NUM) { return scr_dyn_retain(scr_dyn_undefined()); } + if (value->kind == SCR_DYN_OBJ && value->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(value, key->data, key->len); + if (!slot) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *out = scr_dyn_new_obj(); + scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(*slot)); + 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_OBJ) { ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key); if (!entry) return scr_dyn_retain(scr_dyn_undefined()); @@ -3445,7 +3745,24 @@ typedef struct { const char *names; const char *lens; size_t name_stride, len_stride, count; + /* A record that a narrower record type also references (a prefix view, + * scr_dyn_typed_ref_prefix_view) has an entry with no view and this, its + * own capsule's parameters: every capsule of the record then answers + * for its whole storage (scr_dyn_typed_ref_new), and reflective surfaces + * of the narrower type go through that capsule (scr_record_source_has). + * Owned; NULL for an ordinary source. */ + struct ScrRecordPrefix *prefix; } ScrRecordSource; +typedef struct ScrRecordPrefix { + void *(*retain)(void *); + void (*release)(void *); + const char *type_key; + size_t type_key_len; + ScrDyn *(*materialize)(void *); + void (*commit)(void *, const ScrDyn *); + const ScrDynTypedArrayOps *ops; /* the record's ScrDynTypedRecordOps */ + bool traced, record; +} ScrRecordPrefix; static SCR_TL ScrRecordSource *scr_record_sources; static SCR_TL size_t scr_record_source_capacity, scr_record_source_count; /* Deleted slots keep this marker so probe runs stay intact without moving @@ -3476,7 +3793,7 @@ static size_t scr_record_source_bucket(const void *rec) { return (size_t)((run << 6) | (key & 63)) & (scr_record_source_capacity - 1); } -static ScrRecordSource *scr_record_source_find(const void *rec) { +static ScrRecordSource *scr_record_source_slot(const void *rec) { if (!scr_record_source_count) return NULL; size_t mask = scr_record_source_capacity - 1; for (size_t slot = scr_record_source_bucket(rec); scr_record_sources[slot].rec; @@ -3486,6 +3803,17 @@ static ScrRecordSource *scr_record_source_find(const void *rec) { return NULL; } +/* The record's kept source, or NULL (also for a prefix-view entry). */ +static ScrRecordSource *scr_record_source_find(const void *rec) { + ScrRecordSource *entry = scr_record_source_slot(rec); + return entry && !entry->prefix ? entry : NULL; +} + +static const ScrRecordPrefix *scr_record_prefix_find(const void *rec) { + ScrRecordSource *entry = scr_record_source_slot(rec); + return entry ? entry->prefix : NULL; +} + /* The retained view of a record with undeclared keys (borrowed), or NULL. */ ScrDyn *scr_record_source_view(const void *rec) { ScrRecordSource *entry = scr_record_source_find(rec); @@ -3493,7 +3821,7 @@ ScrDyn *scr_record_source_view(const void *rec) { } bool scr_record_source_has(const void *rec) { - return scr_record_source_count && scr_record_source_find(rec) != NULL; + return scr_record_source_count && scr_record_source_slot(rec) != NULL; } /* Drops the entry at `found` from the table, releasing its view. The table @@ -3501,6 +3829,7 @@ bool scr_record_source_has(const void *rec) { * the dispose hook sees a consistent table. */ static void scr_record_source_drop(ScrRecordSource *found) { ScrDyn *view = found->view; + free(found->prefix); *found = (ScrRecordSource){ .rec = SCR_RECORD_SOURCE_DELETED }; scr_record_source_count--; scr_record_source_deleted++; @@ -3510,7 +3839,7 @@ static void scr_record_source_drop(ScrRecordSource *found) { static void scr_record_source_disposed(void *rec) { size_t bit = scr_record_source_filter_bit(rec); if (!(scr_record_source_filter[bit >> 6] & (UINT64_C(1) << (bit & 63)))) return; - ScrRecordSource *found = scr_record_source_find(rec); + ScrRecordSource *found = scr_record_source_slot(rec); if (found) scr_record_source_drop(found); } @@ -3583,7 +3912,10 @@ static void scr_record_source_teardown(void) { free(scr_record_source_filter); scr_record_source_filter = NULL; for (size_t i = 0; i < capacity; i++) { - if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) scr_dyn_release(table[i].view); + if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) { + free(table[i].prefix); + scr_dyn_release(table[i].view); + } } free(table); } @@ -3626,7 +3958,8 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam bool keep = false; size_t next = 0; for (size_t i = 0; i < src->v.obj.len && !keep; i++) { - const ScrDynEntry *entry = &src->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(src, i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); keep = index == count || index < next; next = index + 1; @@ -3634,7 +3967,8 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam if (!keep) return; ScrDyn *view = scr_dyn_new_obj(); for (size_t i = 0; i < src->v.obj.len; i++) { - const ScrDynEntry *entry = &src->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(src, i); + const ScrDynProp *entry = &entry_prop; 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())); } else if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) { @@ -3667,7 +4001,8 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam 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]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(from, i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&probe, entry->key, entry->key_len); if (index == count) continue; keep = index != next; @@ -3676,7 +4011,8 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam 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]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(from, i); + const ScrDynProp *entry = &entry_prop; 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())); } @@ -3687,7 +4023,7 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam 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); + ScrRecordSource *old = scr_record_source_slot(dst); if (old) scr_record_source_drop(old); probe.view = view; scr_record_source_put(&probe); @@ -3695,9 +4031,15 @@ void scr_record_source_spread(void *dst, const void *src, const char *const *nam void scr_record_source_move(void *from, void *to) { if (!scr_record_source_count) return; - ScrRecordSource *old = scr_record_source_find(to); - if (old) scr_record_source_drop(old); + ScrRecordSource *old = scr_record_source_slot(to); ScrRecordSource *found = scr_record_source_find(from); + if (old && old->prefix) { + /* A prefix-viewed record keeps its entry: its capsule's view holds the + * undeclared properties, and narrower names read them through it. */ + if (found) scr_record_source_drop(found); + return; + } + if (old) scr_record_source_drop(old); if (!found) return; ScrRecordSource moved = *found; *found = (ScrRecordSource){ .rec = SCR_RECORD_SOURCE_DELETED }; @@ -3715,7 +4057,8 @@ static ScrDyn *scr_record_source_merge(const ScrRecordSource *source, const ScrD const ScrDyn *fresh) { ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < cached->v.obj.len; i++) { - const ScrDynEntry *entry = &cached->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(cached, i); + const ScrDynProp *entry = &entry_prop; ScrDyn *value = scr_dyn_obj_get(fresh, entry->key, entry->key_len); if (value) { scr_dyn_obj_set(out, entry->key, entry->key_len, scr_dyn_retain(value)); @@ -3736,7 +4079,8 @@ static ScrDyn *scr_record_source_merge(const ScrRecordSource *source, const ScrD scr_str_release(name); } for (size_t i = 0; i < fresh->v.obj.len; i++) { - const ScrDynEntry *entry = &fresh->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(fresh, i); + const ScrDynProp *entry = &entry_prop; if (scr_dyn_obj_get(out, entry->key, entry->key_len)) continue; scr_dyn_obj_set(out, entry->key, entry->key_len, scr_dyn_retain(entry->value)); } @@ -3755,6 +4099,7 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec) { } static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key); +static void scr_jb_put_record_prefix(ScrJsonBuf *b, void *rec); static bool scr_jb_try_pure_member(ScrJsonBuf *b, const char *key, size_t key_len, const ScrStr *key_str, const ScrDyn *value, bool *first); static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first, @@ -3812,7 +4157,8 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { if (!keys) scr_json_oom(); size_t integers = 0, next = 0; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; double index; if (scr_dyn_key_is_index(entry->key, entry->key_len, &index)) keys[integers++] = (ScrRecordSourceIntegerKey){ index, i }; @@ -3821,7 +4167,8 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { qsort(keys, integers, sizeof *keys, scr_record_source_integer_cmp); for (size_t i = 0; i < integers; i++) out[next++] = keys[i].entry; for (size_t i = 0; i < len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; double index; if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) out[next++] = i; } @@ -3830,6 +4177,10 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, bool (*field)(ScrJsonBuf *, void *, size_t, bool)) { + if (scr_record_prefix_find(rec)) { + scr_jb_put_record_prefix(b, rec); + return true; + } ScrRecordSource *found = scr_record_source_find(rec); if (!found) return false; /* Program code run by a member can add record sources and move the @@ -3854,7 +4205,7 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, size_t *order = NULL; for (size_t k = 0; k < view->v.obj.len && !order; k++) { double index; - if (!scr_dyn_key_is_index(view->v.obj.entries[k].key, view->v.obj.entries[k].key_len, &index)) continue; + if (!scr_dyn_key_is_index(scr_dyn_obj_dict_entries(view)[k].key, scr_dyn_obj_dict_entries(view)[k].key_len, &index)) continue; order = view->v.obj.len <= 64 ? order_small : malloc(view->v.obj.len * sizeof *order); if (!order) scr_json_oom(); scr_record_source_key_order(view, order); @@ -3865,7 +4216,8 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, * SerializeJSONObject takes them before reading any value. */ size_t i = 0; for (; i < view->v.obj.len && !scr_exc_pending(); i++) { - const ScrDynEntry *entry = &view->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; size_t index = scr_record_source_index(&source, entry->key, entry->key_len); if (index < count) { if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) @@ -3885,7 +4237,8 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, if (i < view->v.obj.len && !scr_exc_pending()) { ScrDyn *rest = scr_dyn_new_arr(); for (size_t j = i; j < view->v.obj.len; j++) { - const ScrDynEntry *entry = &view->v.obj.entries[order ? order[j] : j]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, order ? order[j] : j); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable && !scr_record_source_declared(&source, entry->key, entry->key_len)) continue; ScrStr *name = scr_str_new(entry->key, entry->key_len); scr_dyn_arr_push(rest, scr_dyn_new_str(name)); @@ -3925,6 +4278,20 @@ static ScrDyn *scr_dyn_typed_ref_new( const char *type_key, size_t type_key_len, ScrDyn *(*materialize)(void *), void (*commit)(void *, const ScrDyn *), bool traced, bool observed) { + const ScrRecordPrefix *prefix = + scr_record_source_count && type_key_len >= 7 && memcmp(type_key, "record:", 7) == 0 + ? scr_record_prefix_find(ptr) + : NULL; + if (prefix) { + /* A record seen through a narrower type: its capsule is its own. */ + retain = prefix->retain; + release = prefix->release; + type_key = prefix->type_key; + type_key_len = prefix->type_key_len; + materialize = prefix->materialize; + commit = prefix->commit; + traced = traced || prefix->traced; + } scr_typed_ref_reserve(); size_t slot = scr_typed_ref_bucket(ptr); for (ScrDyn *value; (value = scr_typed_ref_slots[slot]); slot = (slot + 1) & (scr_typed_ref_capacity - 1)) { @@ -3960,6 +4327,11 @@ static ScrDyn *scr_dyn_typed_ref_new( d->v.typed_ref.record = false; d->v.typed_ref.klass = false; d->v.typed_ref.bag_class = false; + if (prefix) { + /* Bound here: the narrower name's bind keeps them (bind_record). */ + d->v.typed_ref.array = prefix->ops; + d->v.typed_ref.record = prefix->record; + } scr_typed_ref_slots[slot] = d; /* the empty slot that ended the probe */ scr_typed_ref_count++; return d; @@ -4006,6 +4378,32 @@ void *scr_dyn_typed_ref_unbox(const ScrDyn *d) { return d->v.typed_ref.retain(d->v.typed_ref.ptr); } +/* A record capsule's native record for a conversion into a type whose + * storage is a prefix of the record's (the compiler's prefix-view sources): + * the pointer itself (+1), so both names keep one object. The record + * remembers its own capsule parameters (ScrRecordPrefix), so every later + * capsule of it, whichever name boxes it, answers for its whole storage. + * NULL while the record keeps undeclared source properties, whose table + * is keyed by the record's own declaration; the conversion then builds + * its copy as before. */ +void *scr_dyn_typed_ref_prefix_view(const ScrDyn *d) { + void *record = d->v.typed_ref.ptr; + if (!scr_record_prefix_find(record)) { + if (scr_record_source_find(record) || !d->v.typed_ref.record) return NULL; + ScrRecordPrefix *prefix = malloc(sizeof *prefix); + if (!prefix) scr_json_oom(); + *prefix = (ScrRecordPrefix){ + d->v.typed_ref.retain, d->v.typed_ref.release, d->v.typed_ref.type_key, + d->v.typed_ref.type_key_len, d->v.typed_ref.materialize, d->v.typed_ref.commit, + d->v.typed_ref.array, d->v.typed_ref.traced, d->v.typed_ref.record, + }; + ScrRecordSource entry = { .rec = record, .prefix = prefix }; + scr_record_source_put(&entry); + } + return d->v.typed_ref.retain(record); +} + + static void scr_dyn_typed_ref_preserve_child( ScrDyn **fresh_slot, ScrDyn *cached_child) { ScrDyn *fresh_child = *fresh_slot; @@ -4038,7 +4436,7 @@ static void scr_dyn_typed_ref_preserve_children( } if (cached->kind == SCR_DYN_OBJ) { for (size_t i = 0; i < fresh->v.obj.len; i++) { - ScrDynEntry *entry = &fresh->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(fresh)[i]; ScrDyn *cached_child = scr_dyn_obj_get( cached, entry->key, entry->key_len); scr_dyn_typed_ref_preserve_child(&entry->value, cached_child); @@ -4086,7 +4484,7 @@ static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value); } if (!named) return; - for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]); + for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &scr_dyn_obj_dict_entries(named)[i]); scr_dyn_release(named); } @@ -4121,7 +4519,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { fresh = ordered; } else if (record_view) { for (size_t i = 0; i < cached->v.obj.len; i++) { - ScrDynEntry *entry = &cached->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(cached)[i]; if (scr_dyn_obj_get(fresh, entry->key, entry->key_len)) continue; ScrStr *name = scr_str_new(entry->key, entry->key_len); ScrDyn *key = scr_dyn_new_str(name); @@ -4155,6 +4553,23 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { return scr_dyn_retain(capsule->v.typed_ref.materialized); } +static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d); + +/* A capsule's dyn view for a checked conversion or match, which only reads + * it (+1): a live record capsule without a view gets a fresh snapshot that + * is not kept, so the capsule keeps answering from the record; any other + * capsule its cached view (scr_dyn_typed_ref_materialize). */ +ScrDyn *scr_dyn_typed_ref_peek(const ScrDyn *d) { + if (!scr_typed_ref_record_ops(d)) return scr_dyn_typed_ref_materialize(d); + ScrDyn *fresh = d->v.typed_ref.materialize(d->v.typed_ref.ptr); + if (scr_exc_pending()) { + scr_dyn_release(fresh); + return scr_dyn_retain(scr_dyn_undefined()); + } + if (scr_dyn_typed_ref_frozen(d)) scr_dyn_record_view_freeze(fresh); + return fresh; +} + 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 && @@ -4192,7 +4607,9 @@ void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops) { * hold state the record cannot (added properties, a prototype, changed * attributes), and every operation goes through it as before. */ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops) { - if (d && d->kind == SCR_DYN_TYPED_REF) { + /* A capsule binds once; a prefix-viewed record's capsule keeps its own + * table when a narrower name boxes it (scr_dyn_typed_ref_new). */ + if (d && d->kind == SCR_DYN_TYPED_REF && !d->v.typed_ref.record) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = true; d->v.typed_ref.klass = false; @@ -4335,6 +4752,18 @@ static bool scr_typed_ref_record_field(const ScrDynTypedRecordOps *ops, const Sc return false; } +static bool scr_typed_ref_record_field_bytes(const ScrDynTypedRecordOps *ops, const char *key, + size_t key_len, size_t *out) { + for (size_t i = 0; i < ops->count; i++) { + const ScrStr *name = ops->names[i]; + if (name->len == key_len && memcmp(name->data, key, key_len) == 0) { + *out = i; + return true; + } + } + return false; +} + /* +1 own field value, or NULL when `key` is not a present field. */ static ScrDyn *scr_typed_ref_record_get(const ScrDyn *d, const ScrStr *key) { const ScrDynTypedRecordOps *ops = scr_typed_ref_record_ops(d); @@ -4482,6 +4911,51 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { /* Writes an element in place or appends at `length`. False (nothing done) * when the value does not fit the element type or the write could grow the * array by more than one element: the snapshot path owns those outcomes. */ +/* A write of a present declared field of a live record stores into the + * native record itself, so the capsule keeps answering from the record + * instead of turning into a permanent snapshot. A present field keeps its + * own-key position, so order observers see what the snapshot would show. + * Absent optional fields, frozen records, and values the field type does + * not accept take the snapshot path, which reports the mismatch. */ +static bool scr_typed_ref_record_store(ScrDyn *d, const ScrDynTypedRecordOps *ops, size_t index, + const ScrDyn *value) { + void *record = d->v.typed_ref.ptr; + if (!ops->set || scr_rc_frozen(record)) return false; + if (!ops->fixed && !ops->has(record, index)) return false; + return ops->set(record, index, value); +} + +static bool scr_typed_ref_record_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) { + const ScrDynTypedRecordOps *ops = scr_typed_ref_record_ops(d); + size_t index; + if (!ops || !scr_typed_ref_record_field(ops, key, &index)) return false; + return scr_typed_ref_record_store(d, ops, index, value); +} + +/* Object.assign onto a live record from a plain object whose own + * properties are all data properties: each enumerable one that names a + * present declared field stores in place. Answers false at the first key + * it cannot store, leaving every key to the snapshot path, which stores + * the already stored values again unchanged. */ +static bool scr_typed_ref_record_assign(ScrDyn *d, const ScrDyn *src) { + const ScrDynTypedRecordOps *ops = scr_typed_ref_record_ops(d); + if (!ops || !ops->set || src->kind != SCR_DYN_OBJ || src->symbol_properties || src->symbol_keys) + return false; + size_t count = scr_dyn_obj_count(src); + for (size_t i = 0; i < count; i++) { + if (scr_dyn_obj_prop_at(src, i).accessor) return false; + } + for (size_t i = 0; i < count; i++) { + ScrDynProp prop = scr_dyn_obj_prop_at(src, i); + if (!prop.enumerable) continue; + size_t index; + if (!scr_typed_ref_record_field_bytes(ops, prop.key, prop.key_len, &index)) return false; + if (!scr_typed_ref_record_store(d, ops, index, prop.value)) return false; + if (scr_exc_pending()) return true; + } + return true; +} + 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; @@ -4572,8 +5046,8 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) { if (PROBE("isRegExp")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_REGEXP; if (PROBE("isPromise")) return value->kind == SCR_DYN_PROMISE; if (PROBE("isProxy")) return value->kind == SCR_DYN_PROXY; - if (PROBE("isAsyncFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 2) != 0; - if (PROBE("isGeneratorFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & 1) != 0; + if (PROBE("isAsyncFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_ASYNC) != 0; + if (PROBE("isGeneratorFunction")) return value->kind == SCR_DYN_FUNC && (scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_GENERATOR) != 0; if (PROBE("isGeneratorObject")) return scr_dyn_generator(value); if (PROBE("isDate")) return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; if (PROBE("isMapIterator")) return scr_native_iterator_over(value, 2); @@ -4855,16 +5329,16 @@ static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) { scr_bytes_ctor_call, 3, "typed-array-constructor", scr_bytes_elem_name(elem)); ScrDyn *properties = scr_dyn_fn_properties(scr_bytes_ctors[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]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(properties)[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_dict_entries(properties)[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_obj_dict_entries(properties)[properties->v.obj.len - 1].enumerable = false; scr_dyn_release(properties); } return scr_dyn_retain(scr_bytes_ctors[elem]); @@ -5850,7 +6324,7 @@ void scr_dyn_fence_member(ScrDyn *object, const ScrStr *key) { } ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name) { - if (clo->function_kind & 8) { + if (clo->function_kind & SCR_CLOSURE_KIND_CHECKED_ADAPTER) { ScrDyn *original = scr_box_get_ref(clo->caps[0]); scr_closure_release(clo); return original; @@ -5937,7 +6411,7 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * `constructor`: the class value. The cycle it closes through the class * object is one the collector traces. */ scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); - data->v.obj.entries[data->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(data)[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -6009,7 +6483,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) { scr_str_release(prefix); scr_str_release(suffix); ScrClosure *closure = scr_closure_new(NULL, 4); - closure->function_kind = scr_closure_identity(target->v.fn.clo)->function_kind & 3; + closure->function_kind = scr_closure_identity(target->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_CALL_MASK; for (size_t i = 0; i < 3; i++) closure->caps[i] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); closure->caps[3] = scr_box_new(SCR_BOX_STR); scr_box_set_ref(closure->caps[0], scr_dyn_retain(target)); @@ -6069,7 +6543,7 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) { if (cb->null_proto) { /* No constructor name: Node renders inspect(value, { depth: -1 }). */ bool keys = false; - for (size_t i = 0; i < cb->v.obj.len && !keys; i++) keys = cb->v.obj.entries[i].enumerable; + for (size_t i = 0; i < cb->v.obj.len && !keys; i++) keys = scr_dyn_obj_dict_entries(cb)[i].enumerable; d = keys ? "[Object: null prototype]" : "[Object: null prototype] {}"; } else { d = "an instance of Object"; @@ -6717,6 +7191,21 @@ void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrD * attributes copied into it (a frozen instance's view is built the same way * as any other). Consumes value. */ void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { + if (obj->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(obj, key, key_len); + if (slot) { + scr_dyn_region_store(obj, value); + ScrDyn *old = *slot; + *slot = value; + scr_dyn_release(old); + return; + } + bool non_extensible = obj->non_extensible; + obj->non_extensible = false; + scr_dyn_obj_put(obj, key, key_len, value); + obj->non_extensible = non_extensible; + return; + } 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) { @@ -6822,19 +7311,19 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { scr_dyn_obj_set(table, "name", 4, scr_dyn_new_str(name_string)); scr_str_release(name_string); 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; + scr_dyn_obj_dict_entries(table)[i].writable = false; + scr_dyn_obj_dict_entries(table)[i].enumerable = false; } ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v); scr_box_set_ref(box, table); owner->props = box; if (function->v.fn.class_obj) function->v.fn.class_obj->static_data = scr_box_retain(box); - if (owner->function_kind & 4) { + if (owner->function_kind & SCR_CLOSURE_KIND_OWN_PROTOTYPE) { ScrDyn *prototype = scr_dyn_new_obj(); scr_dyn_obj_set(prototype, "constructor", 11, scr_dyn_retain((ScrDyn *)function)); - prototype->v.obj.entries[0].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[0].enumerable = false; scr_dyn_obj_set(table, "prototype", 9, prototype); - ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(table)[table->v.obj.len - 1]; entry->enumerable = false; entry->configurable = false; } @@ -6908,7 +7397,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 & SCR_FN_KIND_MASK) != 4) { + (scr_closure_identity(constructor->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) != SCR_CLOSURE_KIND_OWN_PROTOTYPE) { 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; @@ -7057,7 +7546,7 @@ void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) { * 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 & SCR_FN_KIND_MASK) != 4) + (scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) != SCR_CLOSURE_KIND_OWN_PROTOTYPE) return scr_bytes_construct(callee, args, what); ScrDyn *prototype = scr_dyn_fn_get(callee, "prototype", 9); if (!prototype) return NULL; @@ -7524,7 +8013,7 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { } #ifdef SCR_JS_TIER -/* A read site's answer for a receiver without a JS shape whose property is +/* A read site's answer for a receiver without a layout 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 @@ -9193,7 +9682,7 @@ 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 - ? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_FN_KIND_MASK) + ? (int)(scr_closure_identity(value->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) : 0; } @@ -9308,7 +9797,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { 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)) { - const char *gen_tag = scr_gen_tag_reader(d->v.typed_ref.ptr); + const char *gen_tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr); if (gen_tag[0] == '!') { static const char message[] = "Object.prototype.toString of this iterator is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); @@ -9636,7 +10125,7 @@ static void scr_errdyn_teardown(void); static size_t scr_errdyn_self_refs(const ScrDyn *d) { if (d->kind != SCR_DYN_OBJ) return 0; size_t n = 0; - for (size_t i = 0; i < d->v.obj.len; i++) n += d->v.obj.entries[i].value == d; + for (size_t i = 0; i < d->v.obj.len; i++) n += scr_dyn_obj_dict_entries(d)[i].value == d; return n; } @@ -9644,7 +10133,7 @@ static size_t scr_errdyn_self_refs(const ScrDyn *d) { * first so the release frees it. */ static void scr_errdyn_release_node(ScrDyn *d) { for (size_t i = 0; d->kind == SCR_DYN_OBJ && i < d->v.obj.len; i++) { - ScrDynEntry *entry = &d->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(d)[i]; if (entry->value != d) continue; entry->value = scr_dyn_retain(scr_dyn_undefined()); scr_dyn_release(d); @@ -9819,9 +10308,9 @@ static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); /* Whether an own property is an error's `stack` accessor. */ -static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) { - return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC && - entry->setter->v.fn.thunk == scr_error_stack_set_call; +static bool scr_dyn_setter_is_error_stack(bool accessor, const ScrDyn *setter) { + return accessor && setter && setter->kind == SCR_DYN_FUNC && + setter->v.fn.thunk == scr_error_stack_set_call; } static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { @@ -9838,7 +10327,7 @@ static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args ScrStr *key = scr_str_new("stack", 5); ScrDynEntry *entry = scr_dyn_entry(self, key); scr_str_release(key); - if (entry && scr_dyn_entry_is_error_stack(entry)) { + if (entry && scr_dyn_setter_is_error_stack(entry->accessor, entry->setter)) { ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter; entry->value = scr_dyn_retain(value); entry->getter = entry->setter = NULL; @@ -9866,7 +10355,7 @@ static void scr_error_dyn_add_stack(ScrDyn *d) { scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL); } scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined())); - ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1]; entry->accessor = true; entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]); entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]); @@ -9889,25 +10378,25 @@ static bool scr_dyn_error_view_on_error_prototype(const ScrDyn *object) { 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 */ - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; /* Node lists `stack` first among an error's own properties. */ scr_error_dyn_add_stack(d); if (!e->name_present || !e->message_present) { ScrDyn *prototype = scr_dyn_new_obj(); if (!e->name_present) { scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(e->name)); - prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[prototype->v.obj.len - 1].enumerable = false; } if (!e->message_present) { scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(e->message)); - prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(prototype)[prototype->v.obj.len - 1].enumerable = false; } scr_dyn_release(scr_dyn_set_prototype(d, prototype)); scr_dyn_release(prototype); } if (e->message_present) { scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->message_enumerable; } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, @@ -9921,7 +10410,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(info, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->system_code) scr_dyn_obj_set(info, "code", 4, scr_dyn_new_str(e->system_code)); scr_dyn_obj_set(d, "info", 4, info); - d->v.obj.entries[d->v.obj.len - 1].writable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].writable = false; scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call)); } else if (e->errno_order) { @@ -9947,7 +10436,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { if (e->system_dest) scr_dyn_obj_set(d, "dest", 4, scr_dyn_new_str(e->system_dest)); if (e->error_cause) { scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->cause_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->cause_enumerable; } /* DOMException: `code` is the WebIDL legacy NUMBER (never the errno * string slot), and the options form's cause crosses as itself. */ @@ -9963,20 +10452,20 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { if (e->vt == &scr_error_vts[SCR_ERR_SUPPRESSED] && scr_exc_suppressed_values(e, &suppressed_error, &suppressed_value)) { scr_dyn_obj_set(d, "error", 5, scr_caught_to_dyn(suppressed_error)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; scr_dyn_obj_set(d, "suppressed", 10, scr_caught_to_dyn(suppressed_value)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; } /* AggregateError's own non-enumerable `errors` list, shared with the * typed read so both spellings answer the same array. */ if (e->vt == &scr_error_vts[SCR_ERR_AGGREGATE]) { scr_dyn_obj_set(d, "errors", 6, scr_aggregate_errors((ScrError *)e)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = false; } /* An own `name` is assigned after construction defines the others. */ if (e->name_present) { scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); - d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; + scr_dyn_obj_dict_entries(d)[d->v.obj.len - 1].enumerable = e->name_enumerable; } return d; } @@ -9987,7 +10476,7 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { * has no own `name` or no `after` property. */ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) { if (view->kind != SCR_DYN_OBJ) return; - ScrDynEntry *entries = view->v.obj.entries; + ScrDynEntry *entries = scr_dyn_obj_dict_entries(view); size_t n = view->v.obj.len, from = n, to = n; for (size_t i = 0; i < n; i++) { if (entries[i].key_len == 4 && memcmp(entries[i].key, "name", 4) == 0) from = i; @@ -10000,14 +10489,14 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len) memmove(&entries[from], &entries[from + 1], (n - from - 1) * sizeof *entries); memmove(&entries[at + 1], &entries[at], (n - 1 - at) * sizeof *entries); entries[at] = name; - SCR_JS_SHAPE_RESET(view); free(view->v.obj.index); 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]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(object, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i; } return SIZE_MAX; @@ -10018,7 +10507,8 @@ static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t * 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]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(bag, i); + const ScrDynProp *entry = &entry_prop; 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); @@ -10040,7 +10530,8 @@ static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t 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]; + const ScrDynProp anchor_prop = scr_dyn_obj_prop_at(bag, p); + const ScrDynProp *anchor = &anchor_prop; 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); @@ -10048,7 +10539,7 @@ static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t } 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); + size_t before = scr_dyn_entry_index(view, scr_dyn_obj_dict_entries(bag)[p].key, scr_dyn_obj_dict_entries(bag)[p].key_len); if (before != SIZE_MAX) return before > from ? before - 1 : before; } return from; @@ -10068,16 +10559,17 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e 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; + if (at == SIZE_MAX || scr_dyn_obj_dict_entries(view)[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; + ScrDyn *old = scr_dyn_obj_dict_entries(view)[at].value; + scr_dyn_obj_dict_entries(view)[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]; + const ScrDynProp bag_entry_prop = scr_dyn_obj_prop_at(bag, b); + const ScrDynProp *bag_entry = &bag_entry_prop; 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; @@ -10087,7 +10579,7 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e 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 *entries = scr_dyn_obj_dict_entries(view); 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); @@ -10095,7 +10587,6 @@ void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e moved = true; } if (moved) { - SCR_JS_SHAPE_RESET(view); free(view->v.obj.index); view->v.obj.index = NULL; } @@ -10194,7 +10685,8 @@ void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { e->error_cause = replacement; e->cause_enumerable = false; for (size_t i = 0; cause && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(view, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable; } } @@ -10228,7 +10720,8 @@ ScrError *scr_error_from_dyn(const ScrDyn *d) { scr_error_install_cause_drop(&scr_error_cause_drop_impl); e->error_cause = scr_dyn_retain(cause); for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *entry = &d->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(d, i); + const ScrDynProp *entry = &entry_prop; if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable; } } @@ -11337,6 +11830,25 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { /* 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. */ +/* Failed assignments. Sloppy-mode code ignores an assignment that strict + * mode refuses with a TypeError (a read-only or getter-only property, a + * non-extensible object, a frozen array element). The write paths raise + * the strict TypeError and note its payload here; the sloppy entry points + * (scr_dyn_key_set_sloppy) drop exactly that exception when it reaches + * them, while any other exception (a setter's) keeps propagating. */ +static SCR_TL void *scr_dyn_assign_failure; + +static void scr_dyn_note_assign_failure(void) { + if (scr_exc_pending()) scr_dyn_assign_failure = scr_exc_current_cell()->payload; +} + +static void scr_dyn_sloppy_settle(void) { + if (scr_dyn_assign_failure && scr_exc_pending() && + scr_exc_current_cell()->payload == scr_dyn_assign_failure) + scr_exc_clear(); + scr_dyn_assign_failure = NULL; +} + 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; @@ -11358,13 +11870,25 @@ static void scr_dyn_throw_property(const ScrDyn *object, const char *key, size_t } if (!deleting) scr_jb_puts(&buffer, "'"); scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); + if (!deleting) scr_dyn_note_assign_failure(); } /* 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) { - ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (recv->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(recv, key->data, key->len); + if (slot) { + scr_dyn_region_store(recv, value); + ScrDyn *old = *slot; + *slot = scr_dyn_retain(value); + scr_dyn_release(old); + scr_error_sync_cause(recv, key); + return; + } + } + ScrDynEntry *entry = recv->v.obj.layout ? NULL : scr_dyn_entry(recv, key); if (entry) { if (entry->accessor && entry->setter) { ScrDyn *args[] = {value}; @@ -11396,6 +11920,13 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con ScrDyn *table = current->kind == SCR_DYN_FUNC ? scr_dyn_fn_properties(current) : NULL; const ScrDyn *object = table ? table : current; if (object->kind != SCR_DYN_OBJ) { scr_dyn_release(table); break; } + if (object->v.obj.layout) { + /* A layout-mode prototype's properties are writable data. */ + bool found = scr_layout_find(object->v.obj.layout, key->data, key->len) != UINT32_MAX; + current = found ? NULL : scr_dyn_proto_next(object); + scr_dyn_release(table); + continue; + } ScrDynEntry *inherited = scr_dyn_entry((ScrDyn *)object, key); if (!inherited) { current = scr_dyn_proto_next(object); @@ -11429,6 +11960,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_jb_write(&buffer, key->data, key->len); scr_jb_puts(&buffer, ", object is not extensible"); scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); + scr_dyn_note_assign_failure(); return; } scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value)); @@ -11456,6 +11988,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } if (recv->kind == SCR_DYN_TYPED_REF) { if (scr_typed_ref_array_set(recv, key, value)) return; + if (scr_typed_ref_record_set(recv, key, value)) return; if (scr_event_ref_hooks && scr_event_ref_hooks->set(recv, key, value)) return; ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); scr_dyn_key_set(materialized, key, value); @@ -11494,6 +12027,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (key->len == 6 && memcmp(key->data, "length", 6) == 0) { if (recv->v.arr.frozen) { scr_array_frozen_error(3, recv->v.arr.len, 0, 0, 0); + scr_dyn_note_assign_failure(); return; } double number; @@ -11513,6 +12047,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { while (recv->v.arr.len > length) { if (recv->v.arr.sealed && scr_dyn_arr_has_index(recv, recv->v.arr.len - 1)) { scr_array_frozen_error(5, recv->v.arr.len, (double)(recv->v.arr.len - 1), 0, 0); + scr_dyn_note_assign_failure(); return; } scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); @@ -11532,11 +12067,13 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (scr_dyn_array_prototype_element_fence(recv)) return; 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); + scr_dyn_note_assign_failure(); return; } if (recv->non_extensible && !scr_dyn_arr_has_index(recv, idx)) { /* An absent index, a hole included, is a property to add. */ scr_array_frozen_error(0, 0, (double)idx, 0, 0); + scr_dyn_note_assign_failure(); return; } if (idx > recv->v.arr.len + 1000000) { @@ -11633,6 +12170,19 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); } +/* Sloppy-mode assignments (the compiler's IrModule.sloppyDynWrites). */ +void scr_dyn_key_set_sloppy(ScrDyn *recv, ScrStr *key, ScrDyn *value) { + scr_dyn_assign_failure = NULL; + scr_dyn_key_set(recv, key, value); + scr_dyn_sloppy_settle(); +} + +void scr_dyn_key_set_computed_sloppy(ScrDyn *recv, ScrDyn *key, ScrDyn *value) { + scr_dyn_assign_failure = NULL; + scr_dyn_key_set_computed(recv, key, value); + scr_dyn_sloppy_settle(); +} + void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value) { if (key->kind == SCR_DYN_SYMBOL) { scr_dyn_symbol_key_set(recv, key, value); @@ -11694,7 +12244,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 "); @@ -12507,6 +13103,13 @@ static void scr_json_fit_array(ScrDyn *arr) { static void scr_json_fit_object(ScrDyn *obj) { size_t len = obj->v.obj.len, cap = obj->v.obj.cap; if (len == 0 || len == cap) return; + if (obj->v.obj.layout) { + ScrDyn **slots = scr_json_fit_block(obj->v.obj.slots, len * sizeof *slots); + if (!slots) return; /* keep the larger block */ + obj->v.obj.slots = slots; + obj->v.obj.cap = len; + return; + } for (size_t i = len; i < cap; i++) { ScrDynEntry *entry = &obj->v.obj.entries[i]; scr_dyn_key_release(entry->key, entry->key_len); @@ -12570,6 +13173,7 @@ static ScrDyn *scr_json_array(ScrJsonP *p) { typedef struct ScrJsonShape { char *key; size_t key_len; + const ScrLayout *layout; /* the object's layout after this transition */ uint32_t child; /* first transition; 0 = none (the root is never a child) */ uint32_t sibling; /* next transition from the same layout; 0 = none */ } ScrJsonShape; @@ -12603,13 +13207,14 @@ static inline uint32_t scr_json_shape_follow(ScrJsonP *p, uint32_t shape) { return 0; } -/* Record `entry`'s key as a transition from `shape`; 0 when the budget is - * spent (the object then finishes on the ordinary path). */ -static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrDynEntry *entry) { +/* Record the last key of `layout`, an object's layout after a put appended + * it, as a transition from `shape`; 0 when the budget is spent (the object + * then finishes on the ordinary path). */ +static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrLayout *layout) { if (!p->shapes) { p->shapes = malloc(16 * sizeof *p->shapes); if (!p->shapes) scr_json_oom(); - p->shapes[0] = (ScrJsonShape){0}; + p->shapes[0] = (ScrJsonShape){NULL, 0, &scr_layout_empty, 0, 0}; p->nshapes = 1; p->shape_cap = 16; } @@ -12621,41 +13226,18 @@ static uint32_t scr_json_shape_add(ScrJsonP *p, uint32_t shape, const ScrDynEntr p->shape_cap *= 2; } uint32_t added = p->nshapes++; - ((ScrDynKey *)(entry->key - offsetof(ScrDynKey, bytes)))->rc++; - p->shapes[added] = (ScrJsonShape){entry->key, entry->key_len, 0, p->shapes[shape].child}; + const ScrLayoutKey *key = &layout->keys[layout->count - 1]; + scr_dyn_key_retain(key->key); + p->shapes[added] = (ScrJsonShape){key->key, key->len, layout, 0, p->shapes[shape].child}; p->shapes[shape].child = added; return added; } -/* scr_dyn_obj_put for a fresh parser object and a key it does not hold yet, - * sharing the transition's key bytes. */ -static void scr_dyn_obj_append_shared(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value) { +/* scr_dyn_obj_put for a fresh parser object in layout mode and a key it + * does not hold yet: the transition's layout extends the object's. */ +static void scr_dyn_obj_append_shared(ScrDyn *obj, const ScrLayout *layout, ScrDyn *value) { scr_dyn_region_store(obj, value); - if (obj->v.obj.len == obj->v.obj.cap) { - size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4; - ScrDynEntry *entries = scr_mem_realloc(obj->v.obj.entries, cap * sizeof *entries); - if (!entries) scr_json_oom(); - memset(entries + obj->v.obj.cap, 0, (cap - obj->v.obj.cap) * sizeof *entries); - obj->v.obj.entries = entries; - obj->v.obj.cap = cap; - } - SCR_JS_SHAPE_RESET(obj); - ScrDynEntry *e = &obj->v.obj.entries[obj->v.obj.len++]; - if (e->key != key) { - ((ScrDynKey *)(key - offsetof(ScrDynKey, bytes)))->rc++; - scr_dyn_key_release(e->key, e->key_len); - e->key = key; - } - e->key_len = key_len; - e->value = value; - e->getter = NULL; - e->setter = NULL; - e->accessor = false; - e->fenced = false; - e->writable = true; - e->enumerable = true; - e->configurable = true; - if (obj->v.obj.len >= SCR_DYN_OBJECT_INDEX_MIN) scr_dyn_index_append(obj); + scr_dyn_obj_layout_push(obj, layout, value); } static ScrDyn *scr_json_object(ScrJsonP *p) { @@ -12730,14 +13312,14 @@ static ScrDyn *scr_json_object(ScrJsonP *p) { return NULL; } if (shared_key) { - scr_dyn_obj_append_shared(obj, shared_key, key_len, value); + scr_dyn_obj_append_shared(obj, p->shapes[follow].layout, value); shape = follow; } else { size_t before = obj->v.obj.len; scr_dyn_obj_put(obj, key, key_len, value); /* later duplicate keys win */ if (shaped) { - uint32_t added = !decoded_key && obj->v.obj.len == before + 1 - ? scr_json_shape_add(p, shape, &obj->v.obj.entries[before]) : 0; + uint32_t added = !decoded_key && obj->v.obj.len == before + 1 && obj->v.obj.layout + ? scr_json_shape_add(p, shape, obj->v.obj.layout) : 0; if (added) shape = added; else shaped = false; } @@ -13548,7 +14130,7 @@ static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key) { 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]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(object)[i]; if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue; if (entry->fenced) { scr_dyn_namespace_member_fence(); @@ -13557,12 +14139,11 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { ScrDynEntry removed = *entry; memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry); object->v.obj.len--; - SCR_JS_SHAPE_RESET(object); free(object->v.obj.index); object->v.obj.index = NULL; /* The moved tail aliases a live entry; spare slots own only their own * retained key bytes, never a second pointer to a surviving property. */ - memset(&object->v.obj.entries[object->v.obj.len], 0, sizeof *entry); + memset(&scr_dyn_obj_dict_entries(object)[object->v.obj.len], 0, sizeof *entry); scr_dyn_key_release(removed.key, removed.key_len); scr_dyn_release(removed.value); scr_dyn_release(removed.getter); @@ -13937,7 +14518,8 @@ static bool scr_json_plain_container(const ScrDyn *value) { } if (value->kind != SCR_DYN_OBJ) return false; for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; double index; if (entry->accessor || (!intact && !scr_json_plain_value(entry->value)) || @@ -13960,7 +14542,8 @@ static __attribute__((noinline)) bool scr_json_plain_write(ScrJsonBuf *buffer, c scr_jb_putc(buffer, array ? '[' : '{'); bool first = true; for (size_t i = 0; i < length; i++) { - const ScrDynEntry *entry = array ? NULL : &value->v.obj.entries[i]; + const ScrDynProp entry_prop = array ? (ScrDynProp){0} : scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = array ? NULL : &entry_prop; if (entry && !entry->enumerable) continue; const ScrDyn *child = array ? value->v.arr.items[i] : entry->value; bool omitted = scr_json_omitted(child); @@ -14262,6 +14845,13 @@ ScrStr *scr_util_format_j_text(ScrStr *text) { /* The gap of a compact walk; the walks read only its length. */ static const ScrStr scr_json_compact_gap = { .rc = SIZE_MAX, .len = 0 }; +/* JSON for a prefix-viewed record: its own capsule's. */ +static void scr_jb_put_record_prefix(ScrJsonBuf *b, void *rec) { + ScrDyn *capsule = scr_dyn_new_typed_ref(rec, NULL, NULL, "record:", 7, NULL, NULL); + scr_json_replaced_write(b, capsule, scr_dyn_undefined(), NULL, &scr_json_compact_gap, b->seen_len); + scr_dyn_release(capsule); +} + #define SCR_JSON_PURE_DEPTH 32 /* Writes `value` when writing it runs no program code and meets no cycle: @@ -14296,7 +14886,8 @@ static bool scr_jb_put_pure(ScrJsonBuf *b, const ScrDyn *value, size_t depth) { scr_jb_putc(b, '{'); bool first = true; for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; double index; if (entry->accessor || (entry->key_len == 6 && memcmp(entry->key, "toJSON", 6) == 0) || @@ -14556,7 +15147,7 @@ static ScrDyn *scr_function_constructor_call(ScrClosure *closure, ScrDyn *const } static ScrDyn *scr_function_constructor(uint32_t kind) { - kind &= 3; + kind &= SCR_CLOSURE_KIND_CALL_MASK; static const char *const names[] = {"Function", "GeneratorFunction", "AsyncFunction", "AsyncGeneratorFunction"}; if (!scr_function_constructors_registered) { scr_atexit(scr_function_constructors_cleanup); @@ -14586,7 +15177,7 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { return NULL; } if (!d->v.fn.class_obj && key_len == 9 && !memcmp(key, "prototype", 9) && !owner->props) { - if (owner->function_kind & 1) { + if (owner->function_kind & SCR_CLOSURE_KIND_GENERATOR) { static const char message[] = "Native generator function prototypes have no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -14827,14 +15418,15 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion } ScrDyn *out = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *e = &v->v.obj.entries[i]; - if (scr_dyn_entry_is_error_stack(e)) { + const ScrDynProp e_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *e = &e_prop; + if (scr_dyn_setter_is_error_stack(e->accessor, e->setter)) { /* A stack that was already formatted copies as a non-enumerable * data property; an unformatted one has nothing to copy. */ ScrError *error = scr_errdyn_err_of(v); if (error && error->stack) { scr_dyn_obj_set(out, "stack", 5, scr_dyn_new_str(error->stack)); - out->v.obj.entries[out->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(out)[out->v.obj.len - 1].enumerable = false; } scr_error_release(error); continue; @@ -14947,6 +15539,11 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ continue; } if (current->kind != SCR_DYN_OBJ) continue; + if (current->v.obj.layout) { + ScrDyn **slot = scr_dyn_layout_slot(current, key, key_len); + if (slot) return scr_dyn_retain(*slot); + continue; + } ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len); if (entry) { if (!entry->accessor) return scr_dyn_retain(entry->value); @@ -15072,9 +15669,38 @@ static void scr_dyn_read_ics_reset(void) { scr_dyn_read_ics_registered = false; } +#ifndef SCR_RC_AUDIT +/* Points the site at `key` (shared key bytes). */ +static void scr_dyn_read_ic_key(ScrDynReadIc *ic, char *key, size_t key_len) { + if (ic->key == key) return; + scr_dyn_key_retain(key); + if (ic->key) { + scr_dyn_key_release(ic->key, ic->key_len); + } else { + if (!scr_dyn_read_ics_registered) { + scr_dyn_read_ics_registered = true; + scr_atexit(scr_dyn_read_ics_reset); + } + ic->next = scr_dyn_read_ics; + scr_dyn_read_ics = ic; + } + ic->key = key; + ic->key_len = key_len; +} +#endif + static __attribute__((noinline)) ScrDyn *scr_dyn_obj_read_ic_miss(const ScrDyn *d, const ScrStr *key, ScrDynReadIc *ic) { #ifndef SCR_RC_AUDIT + if (d->kind == SCR_DYN_OBJ && d->v.obj.layout) { + uint32_t i = d != scr_builtin_object_prototype + ? scr_layout_find(d->v.obj.layout, key->data, key->len) : UINT32_MAX; + if (i == UINT32_MAX) return scr_dyn_obj_read_receiver(d, key->data, key->len, d); + const ScrLayoutKey *k = &d->v.obj.layout->keys[i]; + scr_dyn_read_ic_key(ic, k->key, k->len); + ic->index = i; + return scr_dyn_retain(d->v.obj.slots[i]); + } ScrDynEntry *entry = d != scr_builtin_object_prototype ? scr_dyn_find_entry(d, key->data, key->len) : NULL; if (entry && !entry->accessor) { if (ic->key != entry->key) { @@ -15105,6 +15731,16 @@ static __attribute__((noinline)) ScrDyn *scr_dyn_obj_read_ic_miss(const ScrDyn * * do not, and compare their bytes at the remembered position instead. */ ScrDyn *scr_dyn_obj_read_ic(const ScrDyn *d, const ScrStr *key, ScrDynReadIc *ic) { size_t index = ic->index; + const ScrLayout *layout = d->kind == SCR_DYN_OBJ ? d->v.obj.layout : NULL; + if (layout) { + if (index < d->v.obj.len) { + const ScrLayoutKey *k = &layout->keys[index]; + if ((k->key == ic->key || (k->len == key->len && scr_key_equal(k->key, key->data, key->len))) && + d != scr_builtin_object_prototype) + return scr_dyn_retain(d->v.obj.slots[index]); + } + return scr_dyn_obj_read_ic_miss(d, key, ic); + } if (index < d->v.obj.len) { const ScrDynEntry *entry = &d->v.obj.entries[index]; if ((entry->key == ic->key || @@ -15301,7 +15937,8 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { ScrKeyIndex *indices = NULL; size_t count = 0; for (size_t i = 0; i < n; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *entry = &entry_prop; uint32_t index; if (!scr_key_array_index(entry->key, entry->key_len, &index)) continue; if (!indices) { @@ -15318,7 +15955,8 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { for (size_t i = 0; i < count; i++) order[i] = indices[i].entry; free(indices); for (size_t i = 0; i < n; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, i); + const ScrDynProp *entry = &entry_prop; uint32_t index; if (!scr_key_array_index(entry->key, entry->key_len, &index)) order[count++] = i; } @@ -15329,8 +15967,9 @@ static size_t *scr_dyn_obj_key_order(const ScrDyn *v) { * walk over its values runs no user code and the object cannot change * under it. */ static bool scr_dyn_obj_data_only(const ScrDyn *v) { + if (v->v.obj.layout) return true; for (size_t i = 0; i < v->v.obj.len; i++) - if (v->v.obj.entries[i].accessor) return false; + if (scr_dyn_obj_dict_entries(v)[i].accessor) return false; return true; } @@ -15339,7 +15978,8 @@ ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) { ScrDyn *keys = scr_dyn_new_arr(); scr_dyn_arr_reserve(keys, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len)); } free(order); @@ -15552,7 +16192,8 @@ static ScrDyn *scr_dyn_obj_data_walk(const ScrDyn *v, ScrObjWalk mode) { ScrDyn *out = scr_dyn_new_arr(); scr_dyn_arr_reserve(out, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) scr_dyn_objwalk_push(out, mode, entry->key, entry->key_len, entry->value); } @@ -15577,7 +16218,8 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { * the shared numeric/insertion ordering over a names-only table. */ ScrDyn *names = scr_dyn_new_obj(); for (size_t i = 0; i < v->v.proxy.target->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.proxy.target->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v->v.proxy.target, i); + const ScrDynProp *entry = &entry_prop; scr_dyn_obj_set(names, entry->key, entry->key_len, scr_dyn_retain(scr_dyn_undefined())); } raw = scr_dyn_obj_keys(names); @@ -15619,7 +16261,8 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { scr_dyn_release(raw); const ScrDyn *target = v->v.proxy.target; for (size_t i = 0; target->kind == SCR_DYN_OBJ && i < target->v.obj.len; i++) { - const ScrDynEntry *entry = &target->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(target, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable) continue; bool found = false; for (size_t j = 0; j < keys->v.arr.len; j++) { @@ -15888,7 +16531,8 @@ static ScrArr *scr_dyn_own_key_strs(const ScrDyn *v, bool shared) { size_t *order = scr_dyn_obj_key_order(v); ScrArr *out = scr_arr_new(SCR_ELEM_STR, v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) scr_arr_push_ref(out, scr_str_new(entry->key, entry->key_len)); } free(order); @@ -15958,7 +16602,8 @@ ScrDynEntryPairs *scr_dyn_obj_entry_pairs(const ScrDyn *v) { size_t *order = scr_dyn_obj_key_order(v); ScrDynEntryPairs *out = scr_dyn_entry_pairs_new(v->v.obj.len); for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (!entry->enumerable) continue; out->pairs[out->count++] = (ScrDynEntryPair){scr_str_new(entry->key, entry->key_len), scr_dyn_retain(entry->value)}; @@ -16021,7 +16666,8 @@ ScrArr *scr_dyn_obj_value_list(const ScrDyn *v) { uint64_t *slots = scr_dyn_value_slots(v->v.obj.len, stack, 16); size_t count = 0; for (size_t i = 0; i < v->v.obj.len; i++) { - const ScrDynEntry *entry = &v->v.obj.entries[order ? order[i] : i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(v, order ? order[i] : i); + const ScrDynProp *entry = &entry_prop; if (entry->enumerable) slots[count++] = (uint64_t)(uintptr_t)scr_dyn_retain(entry->value); } free(order); @@ -16165,6 +16811,8 @@ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) { return NULL; } + if (target->kind == SCR_DYN_TYPED_REF && scr_typed_ref_record_assign(target, src)) + return scr_exc_pending() ? NULL : scr_dyn_retain(target); if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(target); ScrDyn *assigned = scr_dyn_assign(materialized, src); @@ -16272,7 +16920,7 @@ static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src, ScrDyn *setter = accessor && from->setter ? scr_dyn_retain(from->setter) : NULL; ScrStr *name = slot ? scr_str_new(from->key, from->key_len) : NULL; scr_dyn_obj_set(target, from->key, from->key_len, value); - ScrDynEntry *entry = &target->v.obj.entries[target->v.obj.len - 1]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(target)[target->v.obj.len - 1]; entry->accessor = accessor; entry->getter = getter; entry->setter = setter; @@ -16295,7 +16943,7 @@ ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { if (src->v.obj.len) scr_dyn_region_store(target, NULL); size_t *order = scr_dyn_obj_key_order(src); for (size_t i = 0; i < src->v.obj.len && !scr_exc_pending(); i++) - scr_dyn_copy_property_descriptor(target, src, &src->v.obj.entries[order ? order[i] : i]); + scr_dyn_copy_property_descriptor(target, src, &scr_dyn_obj_dict_entries(src)[order ? order[i] : i]); free(order); } else { ScrDyn *keys = scr_dyn_obj_own_keys(src); @@ -16627,9 +17275,9 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o static void scr_suppressed_attach(ScrError *e, ScrDyn *error, ScrDyn *suppressed) { ScrDyn *view = scr_dyn_from_error(e); scr_dyn_obj_set(view, "error", 5, error); - view->v.obj.entries[view->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(view)[view->v.obj.len - 1].enumerable = false; scr_dyn_obj_set(view, "suppressed", 10, suppressed); - view->v.obj.entries[view->v.obj.len - 1].enumerable = false; + scr_dyn_obj_dict_entries(view)[view->v.obj.len - 1].enumerable = false; scr_dyn_release(view); } @@ -16782,9 +17430,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. */ @@ -16800,61 +17449,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) { @@ -17218,7 +17876,8 @@ static ScrDyn *scr_ffi_open(ScrDyn *catalog, const ScrDyn *path, const ScrDyn *d if (scr_ffi_text_equal(scr_dyn_obj_get(catalog->v.arr.items[i], "library", 7), path)) found = true; if (!found) goto invalid; for (size_t i = 0; i < definitions->v.obj.len; i++) { - const ScrDynEntry *requested = &definitions->v.obj.entries[i]; + const ScrDynProp requested_prop = scr_dyn_obj_prop_at(definitions, i); + const ScrDynProp *requested = &requested_prop; if (!requested->enumerable) continue; if (requested->accessor || requested->value->kind != SCR_DYN_OBJ) goto invalid; ScrDyn *entry = NULL; @@ -18428,7 +19087,7 @@ ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) { value->non_extensible = true; for (size_t i = 0; i < value->v.obj.len; i++) { - ScrDynEntry *entry = &value->v.obj.entries[i]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(value)[i]; entry->configurable = false; if (!entry->accessor) entry->writable = false; } @@ -18464,7 +19123,8 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { 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++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; + const ScrDynProp entry_prop = scr_dyn_obj_prop_at(value, i); + const ScrDynProp *entry = &entry_prop; if (entry->configurable || (!entry->accessor && entry->writable)) return false; } return true; @@ -18526,7 +19186,7 @@ void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) { 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]; + ScrDynEntry *entry = &scr_dyn_obj_dict_entries(table)[i]; if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false; if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false; } @@ -18747,12 +19407,8 @@ bool scr_dyn_iterates_by_protocol(const ScrDyn *value) { const ScrDyn *scr_dyn_tier_object_prototype(void) { return scr_builtin_object_prototype; } -char *scr_dyn_tier_key_copy(const char *key, size_t key_len) { return scr_dyn_key_copy(key, key_len); } - -void scr_dyn_tier_key_release(char *key, size_t key_len) { scr_dyn_key_release(key, key_len); } - -void scr_dyn_tier_append(ScrDyn *obj, char *key, size_t key_len, ScrDyn *value) { - scr_dyn_obj_append_shared(obj, key, key_len, value); +void scr_dyn_tier_append(ScrDyn *obj, const ScrLayout *next, ScrDyn *value) { + scr_dyn_obj_append_shared(obj, next, value); } #endif diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 29ba4bfe3..4ecae73b5 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -10,6 +10,7 @@ #ifdef SCR_RC_AUDIT long scr_str_live_count(void); long scr_dyn_key_live_count(void); +void scr_layout_reset_idle(void); #endif static void put(ScrDyn *map, ScrDyn *key, ScrDyn *value) { @@ -167,7 +168,7 @@ static void checked_storage(void) { char key[16]; int length = snprintf(key, sizeof key, "key-%d", i); scr_dyn_obj_set(object, key, (size_t)length, scr_dyn_new_num(i)); - assert((object->v.obj.index != NULL) == (i >= 31)); + assert((object->v.obj.index != NULL) == (i >= (int)SCR_LAYOUT_MAX_KEYS)); } for (int i = 0; i < 129; i++) { char key[16]; @@ -229,6 +230,8 @@ static void checked_storage(void) { static void shared_property_keys(void) { #ifdef SCR_RC_AUDIT + /* Layouts hold key references until the thread's state is torn down. */ + scr_layout_reset_idle(); long before = scr_dyn_key_live_count(); #endif /* Distinct objects share bytes, not values or property attributes. Key @@ -238,7 +241,8 @@ static void shared_property_keys(void) { char key[] = "shared\0name"; scr_dyn_obj_set(first, key, sizeof key - 1, scr_dyn_new_num(1)); scr_dyn_obj_set(second, key, sizeof key - 1, scr_dyn_new_num(2)); - assert(first->v.obj.entries[0].key == second->v.obj.entries[0].key); + size_t first_len, second_len; + assert(scr_dyn_obj_key_at(first, 0, &first_len) == scr_dyn_obj_key_at(second, 0, &second_len)); key[0] = 'x'; ScrStr *name = scr_str_new("shared\0name", sizeof key - 1); scr_dyn_key_delete(first, name, true); @@ -269,6 +273,7 @@ static void shared_property_keys(void) { } } #ifdef SCR_RC_AUDIT + scr_layout_reset_idle(); assert(scr_dyn_key_live_count() == before); #endif } @@ -651,8 +656,10 @@ static void json_schema_parse(void) { static const char *const order[] = {"tags", "ok", "extra", "qty", "sku"}; assert(view->kind == SCR_DYN_OBJ && view->v.obj.len == 5); for (size_t i = 0; i < 5; i++) { - assert(view->v.obj.entries[i].key_len == strlen(order[i])); - assert(memcmp(view->v.obj.entries[i].key, order[i], strlen(order[i])) == 0); + size_t key_len; + const char *key = scr_dyn_obj_key_at(view, i, &key_len); + assert(key_len == strlen(order[i])); + assert(memcmp(key, order[i], strlen(order[i])) == 0); } assert(scr_dyn_obj_get(view, "extra", 5)->kind == SCR_DYN_OBJ); test_item_release(second); diff --git a/packages/runtime/src/scr_json.test.ts b/packages/runtime/src/scr_json.test.ts index e181674ce..88a89c321 100644 --- a/packages/runtime/src/scr_json.test.ts +++ b/packages/runtime/src/scr_json.test.ts @@ -30,6 +30,7 @@ test.each([false, true])( "scr_json.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 4cfda127b..ad42a5824 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -225,7 +225,18 @@ static void scr_module_registry_cleanup(void) { * own cleanup only when the value is first materialized. That is equivalent * at process exit and retains the RC-audit cleanup guarantee for the values a * program actually observes. */ +#if defined(__linux__) && !defined(SCR_LIB) +/* The heap census of a program built with SCRIPTC_HEAP_CENSUS + * (heap-census.ts) writes its counters at exit, from here and from + * scr_process_exit; every other program leaves the weak reference null. + * Libraries have no census, and their archives declare no such symbol. */ +extern void scr_census_write(void) __attribute__((weak)); +#endif + static void scr_lib_cleanup(void) { +#if defined(__linux__) && !defined(SCR_LIB) + if (scr_census_write != NULL) scr_census_write(); +#endif scr_module_registry_cleanup(); scr_arr_release(scr_argv_arr); scr_argv_arr = NULL; @@ -716,8 +727,9 @@ ScrDyn *scr_process_versions(void) { scr_dyn_obj_set(scr_versions_object, "openssl", 7, scr_dyn_new_str(openssl)); scr_str_release(node); scr_str_release(openssl); - for (size_t i = 0; i < scr_versions_object->v.obj.len; i++) { - scr_versions_object->v.obj.entries[i].writable = false; + for (size_t i = 0; i < scr_dyn_obj_count(scr_versions_object); i++) { + ScrDynProp prop = scr_dyn_obj_prop_at(scr_versions_object, i); + scr_dyn_obj_set_attrs_at(scr_versions_object, i, false, prop.enumerable, prop.configurable); } scr_atexit(&scr_versions_object_cleanup); } @@ -914,8 +926,8 @@ static void scr_env_store(ScrDyn *out, const char *text, size_t ks, size_t ke, } static int scr_env_entry_order(const void *left, const void *right) { - const ScrDynEntry *a = *(const ScrDynEntry *const *)left; - const ScrDynEntry *b = *(const ScrDynEntry *const *)right; + const ScrDynProp *a = left; + const ScrDynProp *b = right; size_t n = a->key_len < b->key_len ? a->key_len : b->key_len; int cmp = memcmp(a->key, b->key, n); return cmp ? cmp : (a->key_len > b->key_len) - (a->key_len < b->key_len); @@ -991,14 +1003,14 @@ static ScrDyn *scr_env_parse_content(const ScrStr *input, bool ordinary) { /* Node's std::map orders UTF-8 keys before installing object properties. * Ordinary objects ignore string-valued __proto__ assignments; loading * the native environment retains that name as an ordinary variable. */ - size_t count = entries->v.obj.len; - const ScrDynEntry **sorted = count ? malloc(count * sizeof(*sorted)) : NULL; + size_t count = scr_dyn_obj_count(entries); + ScrDynProp *sorted = count ? malloc(count * sizeof(*sorted)) : NULL; if (count && !sorted) scr_trap("scriptc: out of memory\n"); - for (size_t i = 0; i < count; i++) sorted[i] = &entries->v.obj.entries[i]; + for (size_t i = 0; i < count; i++) sorted[i] = scr_dyn_obj_prop_at(entries, i); if (count > 1) qsort(sorted, count, sizeof(*sorted), scr_env_entry_order); ScrDyn *result = ordinary ? scr_dyn_new_obj() : scr_dyn_new_obj_null_proto(); for (size_t i = 0; i < count; i++) { - const ScrDynEntry *entry = sorted[i]; + const ScrDynProp *entry = &sorted[i]; if (ordinary && entry->key_len == 9 && memcmp(entry->key, "__proto__", 9) == 0) continue; if (!ordinary) { scr_dyn_obj_set(result, entry->key, entry->key_len, scr_dyn_retain(entry->value)); @@ -1103,15 +1115,15 @@ void scr_process_load_env_file(const ScrDyn *value) { if (!content) return; ScrDyn *entries = scr_env_parse_content(content, false); scr_str_release(content); - for (size_t i = 0; i < entries->v.obj.len; i++) { - const ScrDynEntry *entry = &entries->v.obj.entries[i]; + for (size_t i = 0; i < scr_dyn_obj_count(entries); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(entries, i); /* Node normalizes each raw sorted entry immediately before checking * the environment. Distinct invalid byte keys can normalize alike; * the first absent name wins, rather than the last parsed entry. */ - ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry->key, entry->key_len); + ScrStr *key = scr_str_from_utf8_lossy((const uint8_t *)entry.key, entry.key_len); ScrStr *existing = scr_env_get(key); if (!existing) { - const ScrStr *raw = entry->value->v.str; + const ScrStr *raw = entry.value->v.str; ScrStr *value = scr_str_from_utf8_lossy((const uint8_t *)raw->data, raw->len); scr_env_set(key, value); scr_str_release(value); @@ -2136,6 +2148,9 @@ void scr_process_exit(double code) { fflush(stdout); #if defined(__linux__) if (__llvm_profile_write_file != NULL) (void)__llvm_profile_write_file(); +#ifndef SCR_LIB + if (scr_census_write != NULL) scr_census_write(); +#endif scr_detach_teardown(); #endif _Exit((int)code); @@ -2810,10 +2825,9 @@ ScrDyn *scr_process_release(void) { #endif #undef SCR_RELEASE_FILE #undef SCR_RELEASE_PREFIX - for (size_t i = 0; i < scr_release_object->v.obj.len; i++) { - scr_release_object->v.obj.entries[i].writable = false; - scr_release_object->v.obj.entries[i].configurable = false; - } + for (size_t i = 0; i < scr_dyn_obj_count(scr_release_object); i++) + scr_dyn_obj_set_attrs_at(scr_release_object, i, false, + scr_dyn_obj_prop_at(scr_release_object, i).enumerable, false); scr_atexit(&scr_release_object_cleanup); } return scr_dyn_retain(scr_release_object); @@ -6344,6 +6358,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 diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 7ba9e2907..1d8d91d97 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -150,6 +150,10 @@ void scr_process_exit(double code); * helper process (Linux, `--detached-exit`). scr_lib.c defines a weak 0; * the program object of such an executable defines it as 1. */ extern const unsigned char scr_detached_exit; +/* The heap census writer of a program built with SCRIPTC_HEAP_CENSUS + * (heap-census.ts): that program's object defines it, and scr_lib.c calls + * it at exit through a weak reference (programs on Linux only). */ +void scr_census_write(void); void scr_process_set_exit_code(double code); double scr_process_exit_code_or_zero(void); /* The writer and the presence test of programs that read process.exitCode: @@ -293,6 +297,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 diff --git a/packages/runtime/src/scr_map.test.ts b/packages/runtime/src/scr_map.test.ts index 88aef9c63..b567884fc 100644 --- a/packages/runtime/src/scr_map.test.ts +++ b/packages/runtime/src/scr_map.test.ts @@ -27,6 +27,7 @@ test("number-key maps keep Map semantics across linear, direct and hashed indexe "scr_json.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index d4eb263cf..382907219 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -636,9 +636,9 @@ static bool scr_publish_container_ok(const ScrDyn *d) { if (!scr_dyn_is_frozen(d)) return false; if (d->kind == SCR_DYN_ARR) return d->v.arr.properties == NULL; if (d->null_proto || d->v.obj.source_identity || d->v.obj.source_access) return false; - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *e = &d->v.obj.entries[i]; - if (e->accessor || !e->enumerable) return false; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(d, i); + if (e.accessor || !e.enumerable) return false; } return true; } @@ -666,8 +666,8 @@ static bool scr_publish_collect(ScrPublish *p, ScrDyn *root) { break; case SCR_DYN_OBJ: if (!scr_publish_container_ok(d)) return false; - for (size_t i = 0; i < d->v.obj.len; i++) { - ScrDyn *child = d->v.obj.entries[i].value; + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDyn *child = scr_dyn_obj_value_at(d, i); if (child && !scr_publish_seen(p, child)) scr_publish_push(p, child); } break; @@ -702,9 +702,10 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { } else if (d->rc != SIZE_MAX) { scr_cyc_on_dead(d); /* leave the candidate and tenured buffers */ d->rc = SIZE_MAX; - /* A shape names a node of this thread's transition tree (scr_js.h), - * which other threads cannot use and which ends with this thread. */ - if (d->kind == SCR_DYN_OBJ) d->v.obj.shape = NULL; + /* A layout names a node of this thread's transition tree + * (scr_dyn.h), which other threads cannot use and which ends with + * this thread: the object moves to dictionary mode. */ + if (d->kind == SCR_DYN_OBJ) scr_dyn_to_dictionary(d); #ifdef SCR_RC_AUDIT scr_dyn_live_forget(); #endif @@ -1033,14 +1034,15 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } else scr_dyn_obj_set(values[i], property->key, property->length, value); } if (node->kind == SCR_MESSAGE_ERROR) { - for (size_t j = 0; j < values[i]->v.obj.len; j++) { - ScrDynEntry *entry = &values[i]->v.obj.entries[j]; - entry->enumerable = false; + for (size_t j = 0; j < scr_dyn_obj_count(values[i]); j++) { + ScrDynProp entry = scr_dyn_obj_prop_at(values[i], j); + bool enumerable = false; for (size_t k = 0; k < node->count; k++) { const ScrMessageProperty *property = &node->properties[k]; - if (!property->hidden && property->length == entry->key_len && !memcmp(property->key, entry->key, entry->key_len)) - entry->enumerable = true; + if (!property->hidden && property->length == entry.key_len && !memcmp(property->key, entry.key, entry.key_len)) + enumerable = true; } + scr_dyn_obj_set_attrs_at(values[i], j, entry.writable, enumerable, entry.configurable); } ScrError *error = scr_errdyn_err_of(values[i]); scr_error_commit_dyn(error, values[i]); diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index b6adf34c5..10459255b 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -225,8 +225,11 @@ static void check_large(void) { ScrDyn *decoded = scr_message_decode(message); assert(decoded && !scr_exc_pending()); assert(decoded->v.obj.len == 42); - assert(decoded->v.obj.entries[0].key_len == 5 && !memcmp(decoded->v.obj.entries[0].key, "item0", 5)); - assert(decoded->v.obj.entries[41].key_len == 6 && !memcmp(decoded->v.obj.entries[41].key, "nested", 6)); + size_t member_len; + const char *member = scr_dyn_obj_key_at(decoded, 0, &member_len); + assert(member_len == 5 && !memcmp(member, "item0", 5)); + member = scr_dyn_obj_key_at(decoded, 41, &member_len); + assert(member_len == 6 && !memcmp(member, "nested", 6)); assert(scr_dyn_obj_get(decoded, "nested", 6)->v.num == 42); ScrDyn *first = scr_dyn_obj_get(decoded, "item0", 5); for (int i = 0; i < 40; i++) { diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index a940fb7f1..776829e8c 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -2741,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: diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 6a41e26d8..af4b8e1b8 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -265,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); 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_os.c b/packages/runtime/src/scr_os.c index 8bc476045..637bb7e61 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -745,9 +745,8 @@ double scr_os_cpus_field(double i, double field) { static void scr_os_constant(ScrDyn *o, const char *name, double value) { scr_dyn_obj_set(o, name, strlen(name), scr_dyn_new_num(value)); - ScrDynEntry *entry = &o->v.obj.entries[o->v.obj.len - 1]; - entry->writable = false; - entry->configurable = false; + size_t last = scr_dyn_obj_count(o) - 1; + scr_dyn_obj_set_attrs_at(o, last, false, scr_dyn_obj_prop_at(o, last).enumerable, false); } #define SCR_OS_CONSTANT(o, name) scr_os_constant((o), #name, (double)(name)) @@ -1514,10 +1513,10 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) { scr_os_signal_constants(o); scr_fs_constants(o); if (crypto && crypto->kind == SCR_DYN_OBJ) { - for (size_t i = 0; i < crypto->v.obj.len; i++) { - const ScrDynEntry *entry = &crypto->v.obj.entries[i]; - if (entry->accessor || !entry->value) continue; - scr_dyn_obj_set(o, entry->key, entry->key_len, scr_dyn_retain(entry->value)); + for (size_t i = 0; i < scr_dyn_obj_count(crypto); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(crypto, i); + if (entry.accessor || !entry.value) continue; + scr_dyn_obj_set(o, entry.key, entry.key_len, scr_dyn_retain(entry.value)); } } scr_dyn_release(crypto); @@ -1718,10 +1717,12 @@ double scr_util_signal_exit_code(const ScrDyn *code) { 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++) { + for (size_t i = 0; i < scr_dyn_obj_count(signals); 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); + size_t key_len; + const char *key = scr_dyn_obj_key_at(signals, i, &key_len); + scr_jb_write(&reason, key, key_len); scr_jb_putc(&reason, '\''); } ScrStr *text = scr_jb_finish(&reason); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 8d11d3806..1b29a63aa 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -278,8 +278,8 @@ static bool scr_pub_dyn_ok(const ScrDyn *d, const char **why) { *why = "Cannot publish an Object object"; return false; } - for (size_t i = 0; i < d->v.obj.len; i++) - if (d->v.obj.entries[i].accessor) { + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) + if (scr_dyn_obj_prop_at(d, i).accessor) { *why = "Cannot publish an accessor property"; return false; } @@ -325,9 +325,9 @@ void scr_pub_dyn_fn(void *v, ScrPub *ctx) { case SCR_DYN_ARR: case SCR_DYN_OBJ: { if (!scr_pub_dyn_ok(d, &why)) break; - size_t n = d->kind == SCR_DYN_ARR ? d->v.arr.len : d->v.obj.len; + size_t n = d->kind == SCR_DYN_ARR ? d->v.arr.len : scr_dyn_obj_count(d); for (size_t i = 0; i < n; i++) { - ScrDyn *child = d->kind == SCR_DYN_ARR ? d->v.arr.items[i] : d->v.obj.entries[i].value; + ScrDyn *child = d->kind == SCR_DYN_ARR ? d->v.arr.items[i] : scr_dyn_obj_value_at(d, i); if (child && child->rc != SIZE_MAX) SCR_PUB_ADD((uintptr_t)child); } break; @@ -481,9 +481,9 @@ void scr_publish(void *root, ScrPubFn fn) { void *obj = scr_pub_log_obj(&ctx.log[i]); if (ctx.log[i].obj & SCR_PUB_DYN) { ScrDyn *d = obj; - /* Other threads share the object; its shape belongs to this one - * (scr_js.h). */ - if (d->kind == SCR_DYN_OBJ) d->v.obj.shape = NULL; + /* Other threads share the object; layouts belong to this thread + * (scr_dyn.h), so it moves to dictionary mode. */ + if (d->kind == SCR_DYN_OBJ) scr_dyn_to_dictionary(d); scr_dyn_release(scr_dyn_freeze_data(d)); } #ifdef SCR_RC_AUDIT diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 34c9fdc1d..5a705054a 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -732,6 +732,10 @@ 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. */ diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index 25bbcf7ae..dacd9f163 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -858,12 +858,12 @@ static void scr_rgn_walk_dyn(ScrRgnWalk *w, ScrDyn *d) { break; case SCR_DYN_OBJ: /* source_identity is owned through its access callback: not walked. */ - for (size_t i = 0; i < d->v.obj.len; i++) { - const ScrDynEntry *e = &d->v.obj.entries[i]; - if (e->accessor) { - scr_rgn_enter(w, e->getter, SCR_RGN_W_DYN, NULL); - scr_rgn_enter(w, e->setter, SCR_RGN_W_DYN, NULL); - } else scr_rgn_enter(w, e->value, SCR_RGN_W_DYN, NULL); + for (size_t i = 0; i < scr_dyn_obj_count(d); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(d, i); + if (e.accessor) { + scr_rgn_enter(w, e.getter, SCR_RGN_W_DYN, NULL); + scr_rgn_enter(w, e.setter, SCR_RGN_W_DYN, NULL); + } else scr_rgn_enter(w, e.value, SCR_RGN_W_DYN, NULL); } break; case SCR_DYN_FUNC: scr_rgn_enter(w, d->v.fn.clo, SCR_RGN_W_CLOSURE, NULL); break; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index f19f3918b..5c73917f4 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -56,6 +56,7 @@ #include "scr_blob.h" #include "scr_object.h" #include "scr_region.h" +#include "scr_arena.h" #include "scr_itable.h" #include "scr_slots.h" #include "scr_async.h" diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index fb416e5c3..fdd7179e5 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -119,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 @@ -246,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). @@ -417,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]); @@ -465,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++) { @@ -519,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 { @@ -817,10 +885,14 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { } else { b = chunk; } - 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; + /* 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). */ @@ -969,7 +1041,7 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { } 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); @@ -1938,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); @@ -2437,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) { @@ -2465,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; } @@ -2587,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; @@ -2599,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; @@ -2616,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; @@ -2642,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) */ @@ -7960,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; } diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index c10978298..578712df2 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -681,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 71d629941..cfbb0f980 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -706,31 +706,42 @@ static inline size_t scr_utf16_units_short(const char *data, size_t len) { } /* A fresh concatenation (no head, the template-literal shape): numbers - * format straight into the result, which reserves scr_f64_to_str's whole - * 32-byte buffer for each. */ + * format into a stack buffer first, so the result is allocated at its exact + * length. */ #define SCR_F64_TEXT_MAX 32 static __attribute__((noinline)) ScrStr *scr_str_concat_fresh(ScrStr *const *parts, const double *nums, size_t count) { - size_t reserve = 0; + char texts[SCR_STR_MIXED_MAX * SCR_F64_TEXT_MAX]; + size_t text_len[SCR_STR_MIXED_MAX]; + size_t len = 0, used = 0; bool ascii = true; for (size_t i = 0; i < count; i++) { - size_t n = parts[i] ? parts[i]->len : SCR_F64_TEXT_MAX; - if (n > SCR_STR_MAX_CAP - reserve) scr_oom(); - reserve += n; - if (parts[i]) ascii &= parts[i]->ascii; + size_t n; + if (parts[i]) { + n = parts[i]->len; + ascii &= parts[i]->ascii; + } else { + n = scr_f64_to_str(nums[i], texts + used); + text_len[i] = n; + used += n; + } + if (n > SCR_STR_MAX_CAP - len) scr_oom(); + len += n; } - ScrStr *out = scr_str_take_spare(reserve); - if (!out) out = scr_str_alloc(reserve, reserve); + ScrStr *out = scr_str_take_spare(len); + if (!out) out = scr_str_alloc(len, len); char *w = out->data; + const char *text = texts; for (size_t i = 0; i < count; i++) { if (parts[i]) { scr_copy_bytes(w, parts[i]->data, parts[i]->len); w += parts[i]->len; } else { - w += scr_f64_to_str(nums[i], w); + scr_copy_bytes(w, text, text_len[i]); + w += text_len[i]; + text += text_len[i]; } } - size_t len = (size_t)(w - out->data); out->len = len; out->data[len] = '\0'; out->ascii = ascii; @@ -2120,6 +2131,39 @@ void scr_str_note_utf16_len(ScrStr *s, size_t units) { *scr_u16len_slot(s) = (ScrU16Len){s, units}; } +/* A non-ASCII receiver up to this many bytes (a character, a short word) + * is decoded from its start on each read, as scr_str_utf16_len counts a + * short receiver: an index entry for it would cost more than the walk, and + * would evict the cursor of a long receiver being scanned meanwhile. */ +#define SCR_STR_DECODE_SHORT 16 + +/* The UTF-16 unit at index i of a short receiver, or NaN past its end. */ +static double scr_str_char_code_short(const ScrStr *s, size_t i) { + size_t b = 0; + while (b < s->len) { + unsigned char c = (unsigned char)s->data[b]; + if (c < 0x80) { + if (i == 0) return (double)c; + i--; + b++; + continue; + } + size_t adv; + uint32_t cp = scr_utf8_decode(s->data + b, &adv); + if (cp < 0x10000) { + if (i == 0) return (double)cp; + i--; + } else { + uint32_t v = cp - 0x10000; + if (i == 0) return (double)(0xD800 + (v >> 10)); + if (i == 1) return (double)(0xDC00 + (v & 0x3FF)); + i -= 2; + } + b += adv; + } + return NAN; +} + 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 @@ -2158,6 +2202,7 @@ double scr_str_char_code_at(ScrStr *s, double i) { /* UTF-16 length <= byte length always, so this also fences the cast. */ if (idx >= (double)s->len) return NAN; if (s->ascii) return (double)(unsigned char)s->data[(size_t)idx]; + if (s->len <= SCR_STR_DECODE_SHORT) return scr_str_char_code_short(s, (size_t)idx); ScrSidx *e = scr_sidx(s); if (s->ascii || e->u16len == s->len) return (double)(unsigned char)s->data[(size_t)idx]; if (scr_sidx_units(s, e)) return idx < (double)e->nunits ? (double)e->units[(size_t)idx] : NAN; @@ -2172,11 +2217,19 @@ double scr_str_char_code_at(ScrStr *s, double i) { } double scr_str_index_of(ScrStr *s, ScrStr *needle, double fromIndex) { - /* A search from the start of a proven-ASCII receiver: byte offsets are - * UTF-16 indexes, and a needle with other bytes is never found. A start - * index that is not positive (NaN, -0 or negative) clamps to 0. */ - if (s->ascii && !(fromIndex > 0)) { - const char *found = scr_byte_find(s->data, s->len, needle->data, needle->len); + /* A proven-ASCII receiver: byte offsets are UTF-16 indexes, and a needle + * with other bytes is never found. A start index that is not positive + * (NaN, -0 or negative) clamps to 0, a positive one truncates + * (ToIntegerOrInfinity) and clamps to the length, where only the empty + * needle is found. */ + if (s->ascii) { + size_t start = 0; + if (fromIndex > 0) { + if (fromIndex >= (double)s->len) return needle->len == 0 ? (double)s->len : -1.0; + start = (size_t)fromIndex; + } + const char *found = + scr_byte_find(s->data + start, s->len - start, needle->data, needle->len); return found ? (double)(found - s->data) : -1.0; } double pos = scr_to_integer_or_infinity(fromIndex); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 7e573fc28..372e60702 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -333,7 +333,7 @@ static void scr_dirent_init(void) { scr_dyn_obj_set(proto, methods[i].name, strlen(methods[i].name), scr_dyn_new_func(scr_closure_new(NULL, 0), methods[i].thunk, 0, "native:fs.Dirent", methods[i].name)); - proto->v.obj.entries[proto->v.obj.len - 1].enumerable = false; + scr_dyn_obj_set_attrs_at(proto, scr_dyn_obj_count(proto) - 1, true, false, true); } ScrStr *desc = scr_str_new("type", 4); ScrSym *sym = scr_sym_new(desc); @@ -350,15 +350,14 @@ void scr_dyn_dirent_finish(ScrDyn *obj) { scr_dyn_region_store(obj, NULL); /* a prototype store (ScrDynRegion) */ scr_dyn_release(obj->prototype); obj->prototype = scr_dyn_retain(scr_dirent_prototype); - for (size_t i = 0; i < obj->v.obj.len; i++) { - ScrDynEntry *ent = &obj->v.obj.entries[i]; - if (ent->key_len != 6 || memcmp(ent->key, "%dtype", 6) != 0) continue; - ent->enumerable = false; + size_t i = scr_dyn_obj_index_of(obj, "%dtype", 6); + if (i != SIZE_MAX) { + ScrDynProp ent = scr_dyn_obj_prop_at(obj, i); + scr_dyn_obj_set_attrs_at(obj, i, ent.writable, false, ent.configurable); if (!obj->symbol_properties && !obj->symbol_keys) - scr_dyn_symbol_init_own(obj, scr_dirent_type_key, scr_dirent_type_storage, ent->value); + scr_dyn_symbol_init_own(obj, scr_dirent_type_key, scr_dirent_type_storage, ent.value); else - scr_dyn_symbol_key_set(obj, scr_dirent_type_key, ent->value); - break; + scr_dyn_symbol_key_set(obj, scr_dirent_type_key, ent.value); } } diff --git a/packages/runtime/src/scr_text_pages.c b/packages/runtime/src/scr_text_pages.c new file mode 100644 index 000000000..b99eff644 --- /dev/null +++ b/packages/runtime/src/scr_text_pages.c @@ -0,0 +1,85 @@ +/* scriptc runtime — executable code on transparent huge pages (Linux, + * `scriptc build --large-pages`). + * + * A large program's hot code spans hundreds of 4 KiB pages, more than the + * instruction TLB holds, and most kernels cannot back file-mapped code with + * transparent huge pages. When the system grants huge pages to anonymous + * memory that asks for them (THP "always" or "madvise"), the constructor + * below copies the executable's code segment, from its first to its last + * 2 MiB boundary, into anonymous memory advised MADV_HUGEPAGE, makes the + * copy read-only and executable, and moves it over the original range with + * one mremap. Addresses and instructions stay the same, and the code is + * never writable and executable at once. It runs before main, while the + * process has one thread. If a step fails, the file-mapped code stays. + * + * The moved range is private memory: all of it is resident, not only the + * pages the program runs, and it is not shared with other processes running + * the same executable. Profilers that symbolize from file mappings see it as + * anonymous code. The copy takes about 3 ms for 10 MiB of code, so the unit + * links only into programs built with --large-pages. */ +#include "scr_runtime.h" + +#if defined(__linux__) && !defined(SCR_LIB) +#include +#include +#include +#include + +#if defined(MADV_HUGEPAGE) && defined(MREMAP_FIXED) +#define SCR_LARGE_PAGE ((uintptr_t)2 << 20) + +/* The executable PT_LOAD segment that holds this function: the runtime + * links into the executable, so that is the main program's code. The + * object's name does not identify it: glibc reports the main program with + * an empty name, and musl with its path or "/proc/self/exe". */ +static int scr_text_segment(struct dl_phdr_info *info, size_t size, void *data) { + (void)size; + uintptr_t self = (uintptr_t)&scr_text_segment; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; + if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; + uintptr_t lo = (uintptr_t)(info->dlpi_addr + ph->p_vaddr); + uintptr_t hi = lo + (uintptr_t)ph->p_memsz; + if (self < lo || self >= hi) continue; + uintptr_t *range = data; + range[0] = lo; + range[1] = hi; + return 1; + } + return 0; +} + +/* Whether anonymous memory advised MADV_HUGEPAGE gets huge pages. */ +static bool scr_text_thp_allowed(void) { + FILE *f = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r"); + if (!f) return false; + char buf[96] = {0}; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = 0; + return strstr(buf, "[always]") != NULL || strstr(buf, "[madvise]") != NULL; +} + +__attribute__((constructor)) static void scr_text_large_pages(void) { + uintptr_t range[2] = {0, 0}; + dl_iterate_phdr(scr_text_segment, range); + uintptr_t lo = (range[0] + SCR_LARGE_PAGE - 1) & ~(SCR_LARGE_PAGE - 1); + uintptr_t hi = range[1] & ~(SCR_LARGE_PAGE - 1); + if (range[0] == 0 || hi <= lo || !scr_text_thp_allowed()) return; + size_t len = (size_t)(hi - lo); + /* An aligned anonymous range: map one large page more and trim. */ + char *raw = mmap(NULL, len + SCR_LARGE_PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + if (raw == MAP_FAILED) return; + char *copy = (char *)(((uintptr_t)raw + SCR_LARGE_PAGE - 1) & ~(SCR_LARGE_PAGE - 1)); + char *end = raw + len + SCR_LARGE_PAGE; + if (copy > raw) (void)munmap(raw, (size_t)(copy - raw)); + if (copy + len < end) (void)munmap(copy + len, (size_t)(end - (copy + len))); + (void)madvise(copy, len, MADV_HUGEPAGE); + memcpy(copy, (const void *)lo, len); + if (mprotect(copy, len, PROT_READ | PROT_EXEC) != 0 || + mremap(copy, len, len, MREMAP_MAYMOVE | MREMAP_FIXED, (void *)lo) == MAP_FAILED) + (void)munmap(copy, len); +} +#endif +#endif diff --git a/packages/runtime/src/scr_text_pages.test.c b/packages/runtime/src/scr_text_pages.test.c new file mode 100644 index 000000000..88686e881 --- /dev/null +++ b/packages/runtime/src/scr_text_pages.test.c @@ -0,0 +1,72 @@ +/* scr_text_pages.c checks (Linux): a program that links the unit has its code + * moved onto anonymous memory when the system grants transparent huge pages, + * and keeps the file-mapped code otherwise. The code stays the same either + * way. Eight MiB of padding in the text segment give the move several + * aligned 2 MiB pages to take. */ +#include +#include +#include +#include +#include + +#define PAD_BYTES 0x800000 + +__asm__(".pushsection .text\n" + ".p2align 12\n" + ".globl scr_test_text_pad\n" + "scr_test_text_pad:\n" + ".skip 0x800000, 0xcc\n" + ".popsection\n"); +extern const unsigned char scr_test_text_pad[]; + +static void fail(const char *what) { + printf("FAIL: %s\n", what); + exit(1); +} + +static bool thp_allowed(void) { + FILE *f = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r"); + if (!f) return false; + char buf[96] = {0}; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = 0; + return strstr(buf, "[always]") != NULL || strstr(buf, "[madvise]") != NULL; +} + +/* Whether the mapping holding `p` is backed by a file (an inode), and its + * permissions. */ +static bool mapping_of(const void *p, bool *file, char perms[5]) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return false; + char line[512]; + bool found = false; + while (fgets(line, sizeof line, f)) { + unsigned long lo, hi, offset, inode; + unsigned dev_major, dev_minor; + if (sscanf(line, "%lx-%lx %4s %lx %x:%x %lu", &lo, &hi, perms, &offset, &dev_major, &dev_minor, + &inode) != 7) + continue; + if ((uintptr_t)p < lo || (uintptr_t)p >= hi) continue; + *file = inode != 0; + found = true; + break; + } + fclose(f); + return found; +} + +int main(void) { + const unsigned char *middle = scr_test_text_pad + PAD_BYTES / 2; + bool file; + char perms[5] = {0}; + if (!mapping_of(middle, &file, perms)) fail("the padding is in no mapping"); + if (strcmp(perms, "r-xp") != 0) fail("the code is not read-only and executable"); + bool allowed = thp_allowed(); + if (allowed && file) fail("the code stayed file-mapped although huge pages are allowed"); + if (!allowed && !file) fail("the code moved although huge pages are not allowed"); + for (size_t i = 0; i < PAD_BYTES; i++) + if (scr_test_text_pad[i] != 0xcc) fail("the moved code differs"); + printf("text page checks passed: %s\n", allowed ? "moved" : "kept"); + return 0; +} diff --git a/packages/runtime/src/scr_text_pages.test.ts b/packages/runtime/src/scr_text_pages.test.ts new file mode 100644 index 000000000..7f3c9b3bd --- /dev/null +++ b/packages/runtime/src/scr_text_pages.test.ts @@ -0,0 +1,77 @@ +import { execFile, execFileSync } from "node:child_process"; +import { mkdtemp, readFile, 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); + +function hasZig(): boolean { + try { + execFileSync("zig", ["version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +const sources = [ + join(import.meta.dirname, "scr_text_pages.test.c"), + join(import.meta.dirname, "scr_text_pages.c"), +]; + +/* The code-page move links alone (it needs only libc). Whether it moves the + * code depends on the system's transparent huge page mode. glibc and musl + * report the main program to dl_iterate_phdr under different names, so the + * check runs against both: a glibc build with clang, and a static musl + * build with zig (the compiler musl runtime packs use) where it is + * installed. */ +const lanes = [ + { + name: "glibc", + compiler: "clang", + args: ["-fsanitize=undefined"], + run: true, + }, + { + name: "musl", + compiler: "zig", + args: ["cc", "-target", `${process.arch === "arm64" ? "aarch64" : "x86_64"}-linux-musl`], + run: process.platform === "linux" && hasZig(), + }, +]; + +for (const lane of lanes) { + test.skipIf(process.platform !== "linux" || !lane.run)( + `an opted-in program's code moves onto huge pages when the system allows them (${lane.name})`, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-text-pages-")); + try { + const bin = join(dir, "text-pages"); + await exec(lane.compiler, [ + ...lane.args, + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-D_GNU_SOURCE", + ...sources, + "-o", + bin, + ]); + const mode = await readFile("/sys/kernel/mm/transparent_hugepage/enabled", "utf8").catch( + () => "", + ); + const allowed = mode.includes("[always]") || mode.includes("[madvise]"); + const result = await exec(bin, [], { + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(result.stdout).toBe(`text page checks passed: ${allowed ? "moved" : "kept"}\n`); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + ); +} diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 547fef27a..76ec48e6c 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -2597,21 +2597,21 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } if (!scr_tls_opts_validate(opts)) return false; /* Node's typed errors first */ bool ok = true; - for (size_t i = 0; ok && i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - const ScrDyn *v = e->value; + for (size_t i = 0; ok && i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + const ScrDyn *v = e.value; if (v->kind == SCR_DYN_UNDEF) continue; /* an undefined member reads as absent, like Node */ - if (strcmp(e->key, "cert") == 0 || strcmp(e->key, "key") == 0) { + if (strcmp(e.key, "cert") == 0 || strcmp(e.key, "key") == 0) { char what[96]; - snprintf(what, sizeof what, "a %s '%s' option", api, e->key); + snprintf(what, sizeof what, "a %s '%s' option", api, e.key); ScrBytes *b = scr_tls_pem_dyn(v, what, false); if (b == NULL) { ok = false; break; } - ScrBytes **slot = e->key[0] == 'c' ? cert_out : key_out; + ScrBytes **slot = e.key[0] == 'c' ? cert_out : key_out; scr_bytes_release(*slot); *slot = b; continue; } - if (strcmp(e->key, "ca") == 0) { + if (strcmp(e.key, "ca") == 0) { /* 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 */ @@ -2623,7 +2623,7 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } continue; } - if (strcmp(e->key, "requestCert") == 0) { + if (strcmp(e.key, "requestCert") == 0) { if (auth != NULL) { auth->request = scr_tls_dyn_truthy(v); continue; @@ -2635,22 +2635,22 @@ static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrByt } continue; } - if (strcmp(e->key, "rejectUnauthorized") == 0) { + if (strcmp(e.key, "rejectUnauthorized") == 0) { 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)) + if (server_ok && (strcmp(e.key, "ALPNProtocols") == 0 || strcmp(e.key, "minVersion") == 0 || + strcmp(e.key, "maxVersion") == 0)) continue; - if (strcmp(e->key, "SNICallback") == 0) { + if (strcmp(e.key, "SNICallback") == 0) { scr_tls_opt_fence(api, "SNICallback", "SNICallback lowers only on http2.createSecureServer({ allowHTTP1: true, ... }) with a literal options object"); ok = false; continue; } for (size_t k = 0; SCR_TLS_SRV_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_SRV_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, SCR_TLS_SRV_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e.key, "cert and key (PEM strings or Buffers) are the honestly-implemented members of a runtime options record"); ok = false; break; @@ -2957,17 +2957,17 @@ static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = { * certificates are not modeled), SNICallback and ALPNCallback fence. * False with the exception pending. */ static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api) { - for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { - const ScrDynEntry *e = &record->v.obj.entries[i]; - if (e->value->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "requestCert") == 0) { - if (!scr_tls_dyn_truthy(e->value)) continue; + for (size_t i = 0; record != NULL && i < scr_dyn_obj_count(record); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(record, i); + if (e.value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e.key, "requestCert") == 0) { + if (!scr_tls_dyn_truthy(e.value)) continue; scr_tls_opt_fence(api, "requestCert", "requestCert lowers with cert and key options, not beside a secureContext"); return false; } - if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, "SNICallback") == 0 || strcmp(e.key, "ALPNCallback") == 0) { + scr_tls_opt_fence(api, e.key, "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); return false; } @@ -2980,21 +2980,21 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow static const char api[] = "new tls.TLSSocket"; bool is_server = false; const ScrDyn *record = opts != NULL && opts->kind == SCR_DYN_OBJ ? opts : NULL; - for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { - const ScrDynEntry *e = &record->v.obj.entries[i]; - if (e->value->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "isServer") == 0) { - is_server = scr_tls_dyn_truthy(e->value); + for (size_t i = 0; record != NULL && i < scr_dyn_obj_count(record); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(record, i); + if (e.value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e.key, "isServer") == 0) { + is_server = scr_tls_dyn_truthy(e.value); continue; } - if (strcmp(e->key, "secureContext") == 0) { + if (strcmp(e.key, "secureContext") == 0) { scr_tls_opt_fence(api, "secureContext", "secureContext lowers as a member of an options object literal"); return NULL; } for (size_t k = 0; SCR_TLS_WRAP_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence(api, e->key, + if (strcmp(e.key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e.key, "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); return NULL; } @@ -3267,10 +3267,10 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, /* createSecureContext's version checks come before its other options * (Node's SecureContext constructor). */ if (ok && !scr_tls_versions_dyn(opts, &min_version, &max_version)) ok = false; - for (size_t i = 0; ok && i < opts->v.obj.len; i++) { - const ScrDynEntry *e = &opts->v.obj.entries[i]; - const ScrDyn *v = e->value; - if (strcmp(e->key, "checkServerIdentity") == 0) { + for (size_t i = 0; ok && i < scr_dyn_obj_count(opts); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(opts, i); + const ScrDyn *v = e.value; + if (strcmp(e.key, "checkServerIdentity") == 0) { /* Node spreads user options over the defaults, so a PRESENT key * replaces the builtin verifier even when its value is undefined * — validateFunction then throws for anything non-callable. A @@ -3287,7 +3287,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, continue; } if (v->kind == SCR_DYN_UNDEF) continue; - if (strcmp(e->key, "port") == 0) { + if (strcmp(e.key, "port") == 0) { if (port >= 0) continue; /* the argument form wins, like Node */ if (v->kind == SCR_DYN_NUM) port = v->v.num; else if (v->kind == SCR_DYN_STR) port = strtod(v->v.str->data, NULL); /* Node coerces numeric strings */ @@ -3297,7 +3297,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "host") == 0) { + if (strcmp(e.key, "host") == 0) { if (v->kind == SCR_DYN_STR) opt_host = v->v.str; else { scr_dyn_arg_type_fail("options.host", "of type string", v); @@ -3305,34 +3305,34 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "rejectUnauthorized") == 0) { + if (strcmp(e.key, "rejectUnauthorized") == 0) { reject = scr_tls_dyn_not_false(v); continue; } - if (strcmp(e->key, "ca") == 0) { + if (strcmp(e.key, "ca") == 0) { scr_bytes_release(ca); 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; + 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", + *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) { + if (strcmp(e.key, "ALPNProtocols") == 0) { alpn_value = v; /* converted after the context options, as TLSSocket does */ continue; } - if (strcmp(e->key, "minVersion") == 0 || strcmp(e->key, "maxVersion") == 0) continue; - if (strcmp(e->key, "timeout") == 0) { + if (strcmp(e.key, "minVersion") == 0 || strcmp(e.key, "maxVersion") == 0) continue; + if (strcmp(e.key, "timeout") == 0) { /* `if (options.timeout) tlssock.setTimeout(options.timeout)` */ timeout_value = scr_tls_dyn_truthy(v) ? v : NULL; continue; } - if (strcmp(e->key, "socket") == 0) { + if (strcmp(e.key, "socket") == 0) { if (v->kind == SCR_DYN_HANDLE && v->v.handle.tag == SCR_DYNH_NET_SOCKET) { wrap = (ScrNetSocket *)v->v.handle.ptr; } else { @@ -3341,7 +3341,7 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, } continue; } - if (strcmp(e->key, "servername") == 0) { + if (strcmp(e.key, "servername") == 0) { if (v->kind == SCR_DYN_STR) servername = v->v.str; else { scr_dyn_arg_type_fail("options.servername", "of type string", v); @@ -3350,8 +3350,8 @@ static ScrNetSocket *scr_tls_connect_plain(double port, ScrStr *host /*borrowed, continue; } for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) { - if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { - scr_tls_opt_fence("tls.connect", e->key, + if (strcmp(e.key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence("tls.connect", e.key, "port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); ok = false; break; diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index 17076b078..e85486aea 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -311,7 +311,7 @@ ScrDyn *scr_util_deprecate(ScrDyn *fn, ScrDyn *msg, ScrDyn *code, ScrDyn *option return NULL; } ScrClosure *closure = scr_closure_new(NULL, UC_DEP_CAPS); - closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE; closure->caps[UC_DEP_FN] = uc_box(scr_dyn_retain(fn)); closure->caps[UC_DEP_MSG] = uc_box(scr_dyn_retain(msg)); closure->caps[UC_DEP_CODE] = uc_box(scr_dyn_retain(code)); @@ -557,7 +557,7 @@ ScrDyn *scr_util_callbackify(ScrDyn *original) { return NULL; } ScrClosure *closure = scr_closure_new(NULL, 1); - closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE; closure->caps[0] = uc_box(scr_dyn_retain(original)); ScrDyn *callbackified = scr_dyn_new_func(closure, uc_callbackified_call, 0, "native:util.callbackify", "callbackified"); @@ -708,7 +708,7 @@ static ScrDyn *uc_debug_impl(bool enabled, ScrDyn *set) { scr_str_release(text); } ScrClosure *closure = scr_closure_new(NULL, 1); - closure->function_kind = 4; + closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE; closure->caps[0] = uc_box(scr_dyn_retain(set)); impl = scr_dyn_new_func(closure, uc_debug_write, 0, "native:util.debuglog", "debug"); } else { diff --git a/packages/runtime/src/scr_util_compare.c b/packages/runtime/src/scr_util_compare.c index 5a15b662f..4c2cb9d16 100644 --- a/packages/runtime/src/scr_util_compare.c +++ b/packages/runtime/src/scr_util_compare.c @@ -26,15 +26,15 @@ static bool util_deep_equal(const ScrDyn *, const ScrDyn *, bool, const ScrDeepF static size_t util_deep_key_count(const ScrDyn *table) { size_t count = 0; - if (table) for (size_t i = 0; i < table->v.obj.len; i++) count += table->v.obj.entries[i].enumerable; + if (table) for (size_t i = 0; i < scr_dyn_obj_count(table); i++) count += scr_dyn_obj_prop_at(table, i).enumerable; return count; } static bool util_deep_same_keys(const ScrDyn *a, const ScrDyn *b) { if (util_deep_key_count(a) != util_deep_key_count(b)) return false; - if (a) for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *e = &a->v.obj.entries[i]; - if (e->enumerable && (!b || !scr_dyn_obj_enumerable(b, e->key, e->key_len))) return false; + if (a) for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(a, i); + if (e.enumerable && (!b || !scr_dyn_obj_enumerable(b, e.key, e.key_len))) return false; } return true; } @@ -42,14 +42,14 @@ static bool util_deep_same_keys(const ScrDyn *a, const ScrDyn *b) { static ScrDyn *util_deep_constructor(const ScrDyn *value) { for (const ScrDyn *p = value; p; p = p->prototype) { const ScrDyn *table = p->kind == SCR_DYN_OBJ ? p : p->kind == SCR_DYN_ARR ? p->v.arr.properties : NULL; - if (table) for (size_t i = 0; i < table->v.obj.len; i++) { - const ScrDynEntry *e = &table->v.obj.entries[i]; - if (e->key_len == 11 && !memcmp(e->key, "constructor", 11)) { - if (e->accessor) { util_deep_refused("native accessor properties"); return NULL; } - if (p == value && e->value->kind == SCR_DYN_FUNC) { + if (table) for (size_t i = 0; i < scr_dyn_obj_count(table); i++) { + ScrDynProp e = scr_dyn_obj_prop_at(table, i); + if (e.key_len == 11 && !memcmp(e.key, "constructor", 11)) { + if (e.accessor) { util_deep_refused("native accessor properties"); return NULL; } + if (p == value && e.value->kind == SCR_DYN_FUNC) { util_deep_refused("native own constructor functions"); return NULL; } - return scr_dyn_retain(e->value); + return scr_dyn_retain(e.value); } } if (!p->prototype) { @@ -77,8 +77,8 @@ static ScrDyn *util_deep_tag(const ScrDyn *value) { if (!scr_dyn_same_value(keys->v.arr.items[i], key)) continue; // General native getters retain an explicit boundary, including // non-enumerable Symbol.toStringTag descriptors read by Node. - for (size_t j = 0; j < p->symbol_properties->v.obj.len; j++) { - if (p->symbol_properties->v.obj.entries[j].accessor) { + for (size_t j = 0; j < scr_dyn_obj_count(p->symbol_properties); j++) { + if (scr_dyn_obj_prop_at(p->symbol_properties, j).accessor) { scr_dyn_release(key); util_deep_refused("native accessor properties"); return NULL; } } @@ -95,20 +95,21 @@ static bool util_deep_properties(const ScrDyn *a, const ScrDyn *b, bool skip, const ScrDeepFrame *frame) { if (util_deep_key_count(a) != util_deep_key_count(b)) return false; if (!a) return true; - for (size_t i = 0; i < a->v.obj.len; i++) { - const ScrDynEntry *x = &a->v.obj.entries[i]; - if (!x->enumerable) continue; - const ScrDynEntry *y = NULL; - if (b) for (size_t j = 0; j < b->v.obj.len; j++) { - const ScrDynEntry *candidate = &b->v.obj.entries[j]; - if (candidate->enumerable && candidate->key_len == x->key_len && - !memcmp(candidate->key, x->key, x->key_len)) { y = candidate; break; } + for (size_t i = 0; i < scr_dyn_obj_count(a); i++) { + ScrDynProp x = scr_dyn_obj_prop_at(a, i); + if (!x.enumerable) continue; + ScrDynProp yp; + const ScrDynProp *y = NULL; + if (b) for (size_t j = 0; j < scr_dyn_obj_count(b); j++) { + ScrDynProp candidate = scr_dyn_obj_prop_at(b, j); + if (candidate.enumerable && candidate.key_len == x.key_len && + !memcmp(candidate.key, x.key, x.key_len)) { yp = candidate; y = &yp; break; } } if (!y) return false; - if (x->accessor || y->accessor) return util_deep_refused("native accessor properties"); + if (x.accessor || y->accessor) return util_deep_refused("native accessor properties"); // Materializing a cyclic typed child can refresh its ancestor table. // Retain the values before that refresh replaces descriptor storage. - ScrDyn *xv = scr_dyn_retain(x->value), *yv = scr_dyn_retain(y->value); + ScrDyn *xv = scr_dyn_retain(x.value), *yv = scr_dyn_retain(y->value); bool same = util_deep_equal(xv, yv, skip, frame); scr_dyn_release(xv); scr_dyn_release(yv); if (!same) return false; diff --git a/packages/runtime/src/scr_util_timing.c b/packages/runtime/src/scr_util_timing.c index 13651f5ff..b26839c11 100644 --- a/packages/runtime/src/scr_util_timing.c +++ b/packages/runtime/src/scr_util_timing.c @@ -951,7 +951,7 @@ static UtState *ut_state_new(bool throttle, ScrDyn *signal) { * 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 */ + closure->function_kind = SCR_CLOSURE_KIND_OWN_PROTOTYPE; UtOwner *owner = malloc(sizeof *owner); if (!owner) ut_oom(); owner->rc = 1; diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index a23395c91..7669c4fb9 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -1385,17 +1385,17 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt scr_dyn_arg_type_fail("options", "object", options); return NULL; } - for (size_t i = 0; i < options->v.obj.len; i++) { - const ScrDynEntry *entry = &options->v.obj.entries[i]; - if (entry->accessor) return scr_worker_refusal("Worker option accessors are not supported yet"); - if (entry->key_len == 10 && !memcmp(entry->key, "workerData", 10)) data = entry->value; - else if (entry->key_len == 4 && !memcmp(entry->key, "argv", 4)) argv = entry->value; - else if (entry->key_len == 12 && !memcmp(entry->key, "transferList", 12)) transfer_list = entry->value; - else if (entry->key_len == 14 && !memcmp(entry->key, "resourceLimits", 14)) limits = entry->value; - else if (entry->key_len == 4 && !memcmp(entry->key, "name", 4)) name = entry->value; - else if (entry->key_len == 6 && !memcmp(entry->key, "stdout", 6)) capture |= scr_dyn_truthy(entry->value) ? 1 : 0; - else if (entry->key_len == 6 && !memcmp(entry->key, "stderr", 6)) capture |= scr_dyn_truthy(entry->value) ? 2 : 0; - else if (entry->key_len == 5 && !memcmp(entry->key, "stdin", 5)) capture |= scr_dyn_truthy(entry->value) ? 4 : 0; + for (size_t i = 0; i < scr_dyn_obj_count(options); i++) { + ScrDynProp entry = scr_dyn_obj_prop_at(options, i); + if (entry.accessor) return scr_worker_refusal("Worker option accessors are not supported yet"); + if (entry.key_len == 10 && !memcmp(entry.key, "workerData", 10)) data = entry.value; + else if (entry.key_len == 4 && !memcmp(entry.key, "argv", 4)) argv = entry.value; + else if (entry.key_len == 12 && !memcmp(entry.key, "transferList", 12)) transfer_list = entry.value; + else if (entry.key_len == 14 && !memcmp(entry.key, "resourceLimits", 14)) limits = entry.value; + else if (entry.key_len == 4 && !memcmp(entry.key, "name", 4)) name = entry.value; + else if (entry.key_len == 6 && !memcmp(entry.key, "stdout", 6)) capture |= scr_dyn_truthy(entry.value) ? 1 : 0; + else if (entry.key_len == 6 && !memcmp(entry.key, "stderr", 6)) capture |= scr_dyn_truthy(entry.value) ? 2 : 0; + else if (entry.key_len == 5 && !memcmp(entry.key, "stdin", 5)) capture |= scr_dyn_truthy(entry.value) ? 4 : 0; else return scr_worker_refusal("This Worker constructor option is not supported yet"); } } diff --git a/packages/runtime/src/scr_zlib.c b/packages/runtime/src/scr_zlib.c index 8756c3d17..bc1254f29 100644 --- a/packages/runtime/src/scr_zlib.c +++ b/packages/runtime/src/scr_zlib.c @@ -1,13 +1,10 @@ /* node:zlib, the lowered slice: the one-shot zlib/raw-DEFLATE/gzip codecs * over u8 bytes with Node's DEFAULT options. Compiled ONLY when the program * uses zlib (native-toolchain.ts gates it exactly like scr_regex.c/libregexp), - * so zlib-free binaries keep their historical link line. Host builds link - * the system -lz; cross targets link the vendored zlib built per target - * (ensureZlibObjects in native-toolchain.ts). - * - * Compressed OUTPUT bytes are zlib-version-dependent — the differential - * corpus tests round-trips and fixed-blob inflation, never raw deflate - * output. deflate of valid input cannot fail (OOM aborts); inflate of + * so zlib-free binaries keep their historical link line. Every build links + * the vendored zlib (packages/runtime/vendor/zlib), the copy Node bundles, + * so compressed bytes match Node's for the same parameters. deflate of valid + * input cannot fail (OOM aborts); inflate of * corrupt input THROWS Node's catchable Error (zlib's own msg text: * "incorrect header check", ...). */ #include "scr_runtime.h" diff --git a/packages/runtime/test/array.test.ts b/packages/runtime/test/array.test.ts index 4b42b2beb..ffdafb5e0 100644 --- a/packages/runtime/test/array.test.ts +++ b/packages/runtime/test/array.test.ts @@ -24,6 +24,7 @@ beforeAll(async () => { bin, join(testDir, "test_array.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), diff --git a/packages/runtime/test/bigint.test.ts b/packages/runtime/test/bigint.test.ts index ea0a8b79c..7b6a5e51a 100644 --- a/packages/runtime/test/bigint.test.ts +++ b/packages/runtime/test/bigint.test.ts @@ -32,6 +32,7 @@ beforeAll(async () => { // which pull in the closure/array/box machinery join(testDir, "../src/scr_union.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_bigint.c"), diff --git a/packages/runtime/test/bytes.test.ts b/packages/runtime/test/bytes.test.ts index 7ca0606ec..05123bf5f 100644 --- a/packages/runtime/test/bytes.test.ts +++ b/packages/runtime/test/bytes.test.ts @@ -40,6 +40,7 @@ beforeAll(async () => { join(testDir, "../src/scr_hash.c"), join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_exception.c"), join(testDir, "../src/scr_error.c"), diff --git a/packages/runtime/test/cipher.test.ts b/packages/runtime/test/cipher.test.ts index 6b44a4b4b..44bbc250c 100644 --- a/packages/runtime/test/cipher.test.ts +++ b/packages/runtime/test/cipher.test.ts @@ -20,6 +20,7 @@ const runtimeSources = [ "scr_hash.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/test/closure.test.ts b/packages/runtime/test/closure.test.ts index 96fad6c10..e4c6200dc 100644 --- a/packages/runtime/test/closure.test.ts +++ b/packages/runtime/test/closure.test.ts @@ -25,6 +25,7 @@ test("closure/box runtime: RC cascades clean under ASan + audit", async () => { join(srcDir, "scr_closure.c"), join(srcDir, "scr_string.c"), join(srcDir, "scr_array.c"), + join(srcDir, "scr_arena.c"), join(srcDir, "scr_json.c"), join(srcDir, "scr_bigint.c"), join(srcDir, "scr_map.c"), diff --git a/packages/runtime/test/dh.test.ts b/packages/runtime/test/dh.test.ts index d402d0465..1f324ffb0 100644 --- a/packages/runtime/test/dh.test.ts +++ b/packages/runtime/test/dh.test.ts @@ -20,6 +20,7 @@ const runtimeSources = [ "scr_hash.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/test/glob.test.ts b/packages/runtime/test/glob.test.ts index 92292787a..9900382ea 100644 --- a/packages/runtime/test/glob.test.ts +++ b/packages/runtime/test/glob.test.ts @@ -35,6 +35,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_bigint.c"), join(testDir, "../src/scr_exception.c"), diff --git a/packages/runtime/test/heap-trim.test.ts b/packages/runtime/test/heap-trim.test.ts new file mode 100644 index 000000000..f2ec405fe --- /dev/null +++ b/packages/runtime/test/heap-trim.test.ts @@ -0,0 +1,43 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; + +// scr_heap_trim returns the size-class pages that hold only free blocks, in +// the single-threaded allocator and in the per-thread arenas of worker +// runtimes while another thread allocates. +test.runIf(process.platform === "linux")( + "heap trim returns free pages and keeps live blocks", + async () => { + 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/inspect.test.ts b/packages/runtime/test/inspect.test.ts index 772afe4ef..40d812e53 100644 --- a/packages/runtime/test/inspect.test.ts +++ b/packages/runtime/test/inspect.test.ts @@ -35,6 +35,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_number.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_bytes.c"), join(testDir, "../src/scr_shared.c"), join(testDir, "../src/scr_lib.c"), diff --git a/packages/runtime/test/json.test.ts b/packages/runtime/test/json.test.ts index b08f2fdd3..b049e6922 100644 --- a/packages/runtime/test/json.test.ts +++ b/packages/runtime/test/json.test.ts @@ -29,6 +29,7 @@ beforeAll(async () => { join(testDir, "../src/scr_bigint.c"), join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_exception.c"), join(testDir, "../src/scr_error.c"), diff --git a/packages/runtime/test/lib.test.ts b/packages/runtime/test/lib.test.ts index 2a32202b7..d1e9e8c90 100644 --- a/packages/runtime/test/lib.test.ts +++ b/packages/runtime/test/lib.test.ts @@ -35,6 +35,7 @@ beforeAll(async () => { join(testDir, "../src/scr_path.c"), join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_exception.c"), join(testDir, "../src/scr_error.c"), diff --git a/packages/runtime/test/map.test.ts b/packages/runtime/test/map.test.ts index 4db1889ac..4a56c3055 100644 --- a/packages/runtime/test/map.test.ts +++ b/packages/runtime/test/map.test.ts @@ -33,6 +33,7 @@ beforeAll(async () => { // which pull in the closure/array/box machinery join(testDir, "../src/scr_union.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_bigint.c"), diff --git a/packages/runtime/test/object-core.test.ts b/packages/runtime/test/object-core.test.ts new file mode 100644 index 000000000..ce9e1e3e0 --- /dev/null +++ b/packages/runtime/test/object-core.test.ts @@ -0,0 +1,24 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +const src = join(import.meta.dirname, "..", "src"); + +/* Ordinary objects keep their properties behind the object core: units + * outside it read and adjust them through the scr_dyn.h own-property + * accessors (scr_dyn_obj_count, scr_dyn_obj_prop_at, ...), so the core can + * change how it stores them. */ +const CORE = new Set(["scr_json.c", "scr_js_object.c", "scr_dyn.h"]); + +test("only the object core reads ordinary objects' storage", () => { + const offenders: string[] = []; + for (const name of readdirSync(src)) { + if (!/\.(c|h)$/.test(name) || name.endsWith(".test.c") || CORE.has(name)) continue; + const lines = readFileSync(join(src, name), "utf8").split("\n"); + lines.forEach((line, i) => { + if (/\bv\.obj\.(entries|slots|layout|len|cap|index)\b/.test(line)) + offenders.push(`${name}:${i + 1}: ${line.trim()}`); + }); + } + expect(offenders).toEqual([]); +}); diff --git a/packages/runtime/test/path.test.ts b/packages/runtime/test/path.test.ts index 7f0da7841..79d34c1c3 100644 --- a/packages/runtime/test/path.test.ts +++ b/packages/runtime/test/path.test.ts @@ -29,6 +29,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_number.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), diff --git a/packages/runtime/test/regex.test.ts b/packages/runtime/test/regex.test.ts index 47853d960..cbcbc868b 100644 --- a/packages/runtime/test/regex.test.ts +++ b/packages/runtime/test/regex.test.ts @@ -36,6 +36,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_bigint.c"), join(testDir, "../src/scr_exception.c"), diff --git a/packages/runtime/test/runtime.test.ts b/packages/runtime/test/runtime.test.ts index 11bdf75b4..5b115f868 100644 --- a/packages/runtime/test/runtime.test.ts +++ b/packages/runtime/test/runtime.test.ts @@ -13,6 +13,7 @@ const RUNTIME_SOURCES = [ "scr_bigint.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_bytes.c", "scr_shared.c", "scr_bytes_io.c", diff --git a/packages/runtime/test/sha-kernels.test.ts b/packages/runtime/test/sha-kernels.test.ts index 6f7759348..4cb12a638 100644 --- a/packages/runtime/test/sha-kernels.test.ts +++ b/packages/runtime/test/sha-kernels.test.ts @@ -19,6 +19,7 @@ const runtimeSources = [ "scr_hash.c", "scr_string.c", "scr_array.c", + "scr_arena.c", "scr_map.c", "scr_exception.c", "scr_error.c", diff --git a/packages/runtime/test/sort.test.ts b/packages/runtime/test/sort.test.ts index 711a41f03..fed629b73 100644 --- a/packages/runtime/test/sort.test.ts +++ b/packages/runtime/test/sort.test.ts @@ -24,6 +24,7 @@ beforeAll(async () => { bin, join(testDir, "test_sort.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), diff --git a/packages/runtime/test/string.test.ts b/packages/runtime/test/string.test.ts index 72a479a10..cc6d4fbb6 100644 --- a/packages/runtime/test/string.test.ts +++ b/packages/runtime/test/string.test.ts @@ -30,6 +30,7 @@ beforeAll(async () => { // scr_str_split returns a real string[]: the array module and its // own dependencies join the link (the array.test.ts set). join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), @@ -87,6 +88,13 @@ test("string methods match Node on committed oracle cases", async () => { expect(r.stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/); }); +// A template literal of strings and numbers allocates exactly its length: +// numbers format before the result is sized, not into a reserved maximum. +test("template-literal concatenation allocates its exact length", async () => { + const { stderr } = await execFileAsync(bin, ["--concat-capacity"]); + expect(stderr.trim()).toBe("0 concat capacity failures"); +}); + // repeat(count) with count < 0 or Infinity is Node's catchable RangeError // ("Invalid count value: -1"); the runtime leaves it pending. test.each([ diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index dc48abd26..961d43586 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -833,6 +833,96 @@ static void check_deferred_white_restoration(void) { * scheduling decision or pass would do. */ size_t scr_cyc_pass_at(unsigned gen); void scr_cyc_route_bypassed_at(void); + +/* A nursery pass over a dead ring, longer than the walk recurses, whose + * nodes hold edges into an older ring, and two live nodes the ring reaches + * that hold edges into another: only the dead ring's edges are paid off. */ +static void check_cross_generation_holders(void) { + enum { RING = 200 }; + /* The ring's candidate must wait for the pass below. */ + if (configured_nursery_threshold() < 8) return; + static ScrCycType cyc_type = { &node_trace, &node_free, 0 }; + scr_collect_cycles(); + size_t before = freed; + Node *targets[2]; + for (int r = 0; r < 2; r++) { + targets[r] = make_ring(2); + targets[r]->rc++; + release_live(targets[r]); + } + scr_collect_cycles(); /* both survive, out of the nursery */ + Node *live[2]; + for (int i = 0; i < 2; i++) { + live[i] = scr_cyc_alloc(sizeof(*live[i]), &cyc_type); + live[i]->rc = 3; /* itself, the ring's edge and an outside owner */ + live[i]->next = live[i]; + live[i]->other = targets[1]; + targets[1]->rc++; + } + Node *ring = make_ring(RING); + Node *node = ring; + for (size_t i = 0; i < RING; i++, node = node->next) { + node->other = i == 10 ? live[0] : i == 150 ? live[1] : targets[0]; + if (node->other == targets[0]) targets[0]->rc++; + } + release_live(ring); + scr_cyc_pass_at(SCR_CYC_NURSERY); + check(freed == before + RING, "nursery pass did not free only the dead ring"); + check(targets[0]->rc == 2, "the dead ring's edges into an older ring were not paid off"); + check(targets[1]->rc == 4, "a live node's edge into an older ring was paid off"); + release_live(live[0]); + release_live(live[1]); + scr_collect_cycles(); + check(freed == before + RING + 2, "the live nodes were not collected once dead"); + release_live(targets[0]); + release_live(targets[1]); + scr_collect_cycles(); + check(freed == before + RING + 6, "the older rings were not collected once dead"); +} + +/* A dispose observer (scr_cyc_free notifies it) reads the color of the + * objects a pass tears down. */ +static size_t observed_doomed, observed_other; +static void observe_disposal(void *obj) { + if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_DOOMED) observed_doomed++; + else observed_other++; +} + +static void check_dispose_observer_sees_doomed(void) { + enum { RING = 8 }; + size_t before = freed; + void (*hook)(void *) = scr_weak_dispose_hook; + scr_weak_dispose_hook = observe_disposal; + observed_doomed = observed_other = 0; + Node *ring = make_ring(RING); + release_live(ring); + scr_cyc_pass_at(SCR_CYC_NURSERY); + scr_weak_dispose_hook = hook; + check(freed == before + RING, "the dead ring was not freed"); + check(observed_doomed == RING && observed_other == 0, + "a dispose observer saw a collected object that was not DOOMED"); +} + +/* A full pass whose gray list fills: the dead rings walked before it filled + * stay in the list, and the scan from the candidates passes over them. */ +static void check_doomed_rings_before_full_gray_list(void) { + enum { RINGS = 4, RING = 8, LIVE = 70000 }; + if (configured_nursery_threshold() < 8) return; + size_t before = freed; + Node *dead[RINGS]; + for (int i = 0; i < RINGS; i++) dead[i] = make_ring(RING); + Node *live = make_ring(LIVE); + scr_collect_cycles(); /* re-arms the allocation trigger */ + for (int i = 0; i < RINGS; i++) release_live(dead[i]); + live->rc++; + release_live(live); + scr_collect_cycles(); + check(freed == before + RINGS * RING, "a pass with a full gray list did not free only the dead rings"); + release_live(live); + scr_collect_cycles(); + check(freed == before + RINGS * RING + LIVE, "the large ring was not collected once dead"); +} + static size_t bypass_traced; /* trace calls on bypass-kind nodes */ static size_t bypass_freed; /* bypass-kind nodes freed */ static void bypass_trace(void *obj, ScrTraceVisit visit, void *ctx) { @@ -1314,6 +1404,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 @@ -1579,6 +1707,9 @@ int main(void) { check_deferred_white_restoration(); check_cross_generation_edges(); check_deep_cross_generation_chain(); + check_cross_generation_holders(); + check_dispose_observer_sees_doomed(); + check_doomed_rings_before_full_gray_list(); check_teardown_rebuffers_and_immortals(); check_growth_schedule_follows_yield(); check_growth_cap_bounds_floating_garbage(); @@ -1593,6 +1724,7 @@ 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(); 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_json.c b/packages/runtime/test/test_json.c index 865f1c757..6d492065d 100644 --- a/packages/runtime/test/test_json.c +++ b/packages/runtime/test/test_json.c @@ -571,10 +571,10 @@ static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool } static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) { + const ScrDynEntry *entries = scr_dyn_obj_dict_entries(obj); for (size_t i = 0; i < obj->v.obj.len; i++) - if (obj->v.obj.entries[i].key_len == strlen(key) && - memcmp(obj->v.obj.entries[i].key, key, strlen(key)) == 0) - return &obj->v.obj.entries[i]; + if (entries[i].key_len == strlen(key) && memcmp(entries[i].key, key, strlen(key)) == 0) + return &entries[i]; return NULL; } @@ -618,7 +618,7 @@ static void test_copy_property_descriptors(void) { scr_dyn_obj_set(existing, "label", 5, scr_dyn_new_num(9)); scr_dyn_release(scr_dyn_copy_property_descriptors(existing, bag)); const ScrDynEntry *label = entry_of(existing, "label"); - check(!scr_exc_pending() && existing->v.obj.len == 5 && existing->v.obj.entries[0].key_len == 5 && + check(!scr_exc_pending() && existing->v.obj.len == 5 && scr_dyn_obj_dict_entries(existing)[0].key_len == 5 && label->value->v.num == 1, "copy redefines an existing property in place"); diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index f41c2de50..5a3bdfe5f 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -605,10 +605,15 @@ static void sparse_slice_churn_asserts(void) { static void index_activity_asserts(void) { scr_sidx_test_reset_cache(); ScrStr *strings[9]; + /* A receiver of up to 16 bytes is decoded on each indexed read. */ + ScrStr *shortest = scr_str_new("\xc3\xa9x", 3); + if (scr_str_char_code_at(shortest, 1) != 'x' || scr_sidx_test_active() != 0) + sidx_fail("short indexed read claimed a cursor"); + scr_str_release(shortest); for (size_t i = 0; i < 9; i++) { - strings[i] = scr_str_new("\xc3\xa9x", 3); + strings[i] = scr_str_new("\xc3\xa9xxxxxxxxxxxxxxxxx", 19); /* Short lengths count directly; an indexed read claims a cursor. */ - if (scr_str_utf16_len(strings[i]) != 2 || scr_sidx_test_active() != (i < 4 ? i : 4)) + if (scr_str_utf16_len(strings[i]) != 18 || scr_sidx_test_active() != (i < 4 ? i : 4)) sidx_fail("short length claimed a cursor"); if (scr_str_char_code_at(strings[i], 1) != 'x') sidx_fail("cursor activation read"); if (scr_sidx_test_active() != (i < 4 ? i + 1 : 4)) @@ -616,7 +621,7 @@ static void index_activity_asserts(void) { } for (size_t i = 0; i < 9; i++) scr_str_release(strings[i]); if (scr_sidx_test_active() != 0) sidx_fail("last cursor release stayed active"); - ScrStr *s = scr_str_new("\xe4\xb8\xad", 3); + ScrStr *s = scr_str_new("\xe4\xb8\xadxxxxxxxxxxxxxxxx", 19); (void)scr_str_utf16_len(s); scr_sidx_test_reset_cache(); if (scr_sidx_test_active() != 0) sidx_fail("cache reset stayed active"); @@ -982,7 +987,42 @@ static void ordering_asserts(void) { } } +/* Template-literal concatenations (no head) of strings and numbers: the + * result holds exactly its bytes, whatever the numbers format to. */ +static int check_concat_capacity(void) { + ScrStr *a = scr_str_new("f", 1), *b = scr_str_new("_d", 2), *c = scr_str_new("", 0); + struct { + ScrStr *parts[4]; + double nums[4]; + size_t count; + const char *expected; + } cases[] = { + {{a, NULL, b, NULL}, {0, 7, 0, 2230}, 4, "f7_d2230"}, + {{NULL, a}, {-1.5e-7, 0}, 2, "-1.5e-7f"}, + {{NULL, NULL, NULL}, {1.7976931348623157e308, 0.1, -0.0}, 3, "1.7976931348623157e+3080.10"}, + {{c, NULL, c}, {0, 42, 0}, 3, "42"}, + }; + int failed = 0; + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + ScrStr *r = scr_str_concat_mixed(NULL, cases[i].parts, cases[i].nums, cases[i].count); + size_t want = strlen(cases[i].expected); + if (r->len != want || memcmp(r->data, cases[i].expected, want) != 0 || r->data[want] != '\0' || + r->cap != r->len) { + fprintf(stderr, "concat case %zu: got \"%.*s\" len %zu cap %zu, want \"%s\" len %zu\n", i, + (int)r->len, r->data, r->len, (size_t)r->cap, cases[i].expected, want); + failed++; + } + scr_str_release(r); + } + scr_str_release(a); + scr_str_release(b); + scr_str_release(c); + fprintf(stderr, "%d concat capacity failures\n", failed); + return failed != 0; +} + int main(int argc, char **argv) { + if (argc > 1 && strcmp(argv[1], "--concat-capacity") == 0) return check_concat_capacity(); if (argc > 1 && strncmp(argv[1], "--crash-repeat", 14) == 0) { ScrStr *s = scr_str_new("ab", 2); double count = strcmp(argv[1], "--crash-repeat-inf") == 0 diff --git a/packages/runtime/test/tonumber.test.ts b/packages/runtime/test/tonumber.test.ts index 2a3c24575..0a95f2bb6 100644 --- a/packages/runtime/test/tonumber.test.ts +++ b/packages/runtime/test/tonumber.test.ts @@ -37,6 +37,7 @@ test("scr_string_to_number and scr_parse_float match Node on committed oracle ca // scr_string.c's own link closure (the string.test.ts set): // scr_str_split pulls the array module and its dependencies. join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), diff --git a/packages/runtime/test/url-legacy.test.ts b/packages/runtime/test/url-legacy.test.ts index a237382ae..0af2f7768 100644 --- a/packages/runtime/test/url-legacy.test.ts +++ b/packages/runtime/test/url-legacy.test.ts @@ -38,6 +38,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_map.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_bigint.c"), join(testDir, "../src/scr_exception.c"), diff --git a/packages/runtime/test/url.test.ts b/packages/runtime/test/url.test.ts index 9876ec4e5..ea6948487 100644 --- a/packages/runtime/test/url.test.ts +++ b/packages/runtime/test/url.test.ts @@ -29,6 +29,7 @@ beforeAll(async () => { join(testDir, "../src/scr_string.c"), join(testDir, "../src/scr_number.c"), join(testDir, "../src/scr_array.c"), + join(testDir, "../src/scr_arena.c"), // Checked-value collection equality shares the native JSON value runtime. join(testDir, "../src/scr_json.c"), join(testDir, "../src/scr_map.c"), diff --git a/packages/runtime/vendor/README.md b/packages/runtime/vendor/README.md index 53238410e..cfb0d196f 100644 --- a/packages/runtime/vendor/README.md +++ b/packages/runtime/vendor/README.md @@ -38,13 +38,15 @@ The native runtime pack build produces `libscriptc-quickjs.a` for the compiler b ## zlib/ -- Project: https://github.com/madler/zlib -- Version: 1.3.1 (release tarball zlib-1.3.1.tar.gz, sha256 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23) +- Project: https://chromium.googlesource.com/chromium/src/third_party/zlib (Chromium's zlib, a fork of https://github.com/madler/zlib) +- Version: 1.3.2.1-motley-285e94b, the copy Node v24.22.0 bundles in `deps/zlib` (`process.versions.zlib`) - License: zlib (see zlib/LICENSE) -The CROSS-target arm of node:zlib (see packages/runtime/src/scr_zlib.c): host builds keep the historical system `-lz` link (macOS ships libz), but zig's cross sysroots have no libz, so SCRIPTC_TARGET builds compile this vendored copy per target instead. Only the flat `*.c`/`*.h` at the distribution root are vendored (LICENSE beside them) — contrib, tests, build machinery, and docs are not. No vendored file is modified; the gz* file-I/O TUs are vendored for faithfulness but never compiled (nothing references the gzFile API — see ZLIB_SOURCES in packages/compiler/src/backend/vendor-archives.ts). To update, re-fetch the release tarball, copy the same flat set, and bump `ZLIB_VERSION` in vendor-archives.ts. +The codec behind node:zlib's gzip, deflate and inflate functions and streams (see packages/runtime/src/scr_zlib.c) and the native fetch's gzip decoder. The flat `*.c`/`*.h` files at the root of Node's `deps/zlib`, `contrib/optimizations/`, `LICENSE` and `README.chromium` are vendored; `contrib/minizip`, the other contrib directories, tests, patches, Chromium's build files and docs are not. No vendored file is modified, and the files match Node's copy. The gz* file-I/O units are vendored but never compiled (nothing references the gzFile API). -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. +Each target compiles the units and defines Node's `deps/zlib/zlib.gyp` selects for it: the SSSE3 Adler-32 and SSE2 inflate and slide-hash code on x86-64, the NEON code and the ARMv8 CRC-32 code on arm64 Linux, Android and macOS, the NEON inflate and slide-hash code on iOS and Windows arm64, and the portable code (`CPU_NO_SIMD`) elsewhere. The table is `zlibInputs` in packages/runtime-pack-common/scripts/zlib-inputs.mjs (runtime packs) and in packages/compiler/src/backend/vendor-inputs.ts (Zig cross builds and the sanitized runtime), and a unit test keeps the two identical. Android reads CPU features through `getauxval` as Linux does, where Node's Android build uses the NDK's cpu-features library. The SIMD units check the CPU at run time and give the same output as the portable code, so compressed bytes match Node's for the same parameters on every target. To update, copy the same files from the `deps/zlib` of the Node version scriptc follows and bump `ZLIB_VERSION` in vendor-inputs.ts. + +Runtime packs archive zlib at `-O3`, as Node builds it. Zig cross builds compile the objects 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 compile and link none of it. ## brotli/ diff --git a/packages/runtime/vendor/zlib/LICENSE b/packages/runtime/vendor/zlib/LICENSE index ab8ee6f71..b7a69d058 100644 --- a/packages/runtime/vendor/zlib/LICENSE +++ b/packages/runtime/vendor/zlib/LICENSE @@ -1,6 +1,6 @@ Copyright notice: - (C) 1995-2022 Jean-loup Gailly and Mark Adler + (C) 1995-2026 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages diff --git a/packages/runtime/vendor/zlib/LICENSE.chromium b/packages/runtime/vendor/zlib/LICENSE.chromium new file mode 100644 index 000000000..2249a2865 --- /dev/null +++ b/packages/runtime/vendor/zlib/LICENSE.chromium @@ -0,0 +1,27 @@ +// Copyright 2015 The Chromium Authors +// +// 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 LLC 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. diff --git a/packages/runtime/vendor/zlib/README.chromium b/packages/runtime/vendor/zlib/README.chromium new file mode 100644 index 000000000..232ef66ae --- /dev/null +++ b/packages/runtime/vendor/zlib/README.chromium @@ -0,0 +1,38 @@ +Name: zlib +Short Name: zlib +URL: https://github.com/madler/zlib +Version: 1.3.2 +Revision: 09a1572aa624e5ddb6c075dc013880de70b1b9b9 +Update Mechanism: Manual (https://crbug.com/422348588) +CPEPrefix: cpe:/a:zlib:zlib:1.3.2 +Security Critical: yes +Shipped: yes +License: Zlib +License File: LICENSE +License Android Compatible: yes + +Description: +"A massively spiffy yet delicately unobtrusive compression library." + +zlib is a free, general-purpose, legally unencumbered lossless data-compression +library. zlib implements the "deflate" compression algorithm described by RFC +1951, which combines the LZ77 (Lempel-Ziv) algorithm with Huffman coding. zlib +also implements the zlib (RFC 1950) and gzip (RFC 1952) wrapper formats. + +Local Modifications: + - Only source code from the http://zlib.net distribution used to build the zlib + and minizip libraries are present. Many other files have been omitted. Only *.c + and *.h files from the upstream root directory, contrib/minizip and + examples/zpipe.c were imported. + - The files named '*simd*' are original x86/Arm/RISC-V specific optimizations. + - The contents of the google directory are original Chromium-specific + additions. + - The contents of the 'contrib' of directory are either Chromium-specific + additions or heavily patched zlib files (e.g. inffast_chunk*). + - Added chromeconf.h + - Plus the changes in 'patches' folder. + - Code in contrib/ other than contrib/minizip was added to match zlib's + contributor layout. + - In sync with 1.3.2 official release + - ZIP reader modified to allow for progress callbacks during extraction. + - ZIP reader modified to add detection of AES encrypted content. diff --git a/packages/runtime/vendor/zlib/adler32.c b/packages/runtime/vendor/zlib/adler32.c index 04b81d29b..de78b4e56 100644 --- a/packages/runtime/vendor/zlib/adler32.c +++ b/packages/runtime/vendor/zlib/adler32.c @@ -57,10 +57,27 @@ # define MOD63(a) a %= BASE #endif +#include "cpu_features.h" +#if defined(ADLER32_SIMD_SSSE3) || defined(ADLER32_SIMD_NEON) || defined(ADLER32_SIMD_RVV) +#include "adler32_simd.h" +#endif + /* ========================================================================= */ uLong ZEXPORT adler32_z(uLong adler, const Bytef *buf, z_size_t len) { unsigned long sum2; unsigned n; + /* TODO(cavalcantii): verify if this lengths are optimal for current CPUs. */ +#if defined(ADLER32_SIMD_SSSE3) || defined(ADLER32_SIMD_NEON) \ + || defined(ADLER32_SIMD_RVV) +#if defined(ADLER32_SIMD_SSSE3) + if (buf != Z_NULL && len >= 64 && x86_cpu_enable_ssse3) +#elif defined(ADLER32_SIMD_NEON) + if (buf != Z_NULL && len >= 64) +#elif defined(ADLER32_SIMD_RVV) + if (buf != Z_NULL && len >= 32 && riscv_cpu_enable_rvv) +#endif + return adler32_simd_(adler, buf, len); +#endif /* split Adler-32 into component sums */ sum2 = (adler >> 16) & 0xffff; @@ -77,9 +94,25 @@ uLong ZEXPORT adler32_z(uLong adler, const Bytef *buf, z_size_t len) { return adler | (sum2 << 16); } +#if defined(ADLER32_SIMD_SSSE3) || defined(ADLER32_SIMD_NEON) \ + || defined(RISCV_RVV) + /* + * Use SIMD to compute the adler32. Since this function can be + * freely used, check CPU features here. zlib convention is to + * call adler32(0, NULL, 0), before making calls to adler32(). + * So this is a good early (and infrequent) place to cache CPU + * features for those later, more interesting adler32() calls. + */ + if (buf == Z_NULL) { + if (!len) /* Assume user is calling adler32(0, NULL, 0); */ + cpu_check_features(); + return 1L; + } +#else /* initial Adler-32 value (deferred check for len == 1 speed) */ if (buf == Z_NULL) return 1L; +#endif /* in case short lengths are provided, keep it somewhat fast */ if (len < 16) { diff --git a/packages/runtime/vendor/zlib/adler32_simd.c b/packages/runtime/vendor/zlib/adler32_simd.c new file mode 100644 index 000000000..ce4ce7c57 --- /dev/null +++ b/packages/runtime/vendor/zlib/adler32_simd.c @@ -0,0 +1,459 @@ +/* adler32_simd.c + * + * Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + * + * Per http://en.wikipedia.org/wiki/Adler-32 the adler32 A value (aka s1) is + * the sum of N input data bytes D1 ... DN, + * + * A = A0 + D1 + D2 + ... + DN + * + * where A0 is the initial value. + * + * SSE2 _mm_sad_epu8() can be used for byte sums (see http://bit.ly/2wpUOeD, + * for example) and accumulating the byte sums can use SSE shuffle-adds (see + * the "Integer" section of http://bit.ly/2erPT8t for details). Arm NEON has + * similar instructions. + * + * The adler32 B value (aka s2) sums the A values from each step: + * + * B0 + (A0 + D1) + (A0 + D1 + D2) + ... + (A0 + D1 + D2 + ... + DN) or + * + * B0 + N.A0 + N.D1 + (N-1).D2 + (N-2).D3 + ... + (N-(N-1)).DN + * + * B0 being the initial value. For 32 bytes (ideal for garden-variety SIMD): + * + * B = B0 + 32.A0 + [D1 D2 D3 ... D32] x [32 31 30 ... 1]. + * + * Adjacent blocks of 32 input bytes can be iterated with the expressions to + * compute the adler32 s1 s2 of M >> 32 input bytes [1]. + * + * As M grows, the s1 s2 sums grow. If left unchecked, they would eventually + * overflow the precision of their integer representation (bad). However, s1 + * and s2 also need to be computed modulo the adler BASE value (reduced). If + * at most NMAX bytes are processed before a reduce, s1 s2 _cannot_ overflow + * a uint32_t type (the NMAX constraint) [2]. + * + * [1] the iterative equations for s2 contain constant factors; these can be + * hoisted from the n-blocks do loop of the SIMD code. + * + * [2] zlib adler32_z() uses this fact to implement NMAX-block-based updates + * of the adler s1 s2 of uint32_t type (see adler32.c). + */ + +#include "adler32_simd.h" + +/* Definitions from adler32.c: largest prime smaller than 65536 */ +#define BASE 65521U +/* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */ +#define NMAX 5552 + +#if defined(ADLER32_SIMD_SSSE3) + +#include + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((__target__("ssse3"))) +#endif +uint32_t ZLIB_INTERNAL adler32_simd_( /* SSSE3 */ + uint32_t adler, + const unsigned char *buf, + z_size_t len) +{ + /* + * Split Adler-32 into component sums. + */ + uint32_t s1 = adler & 0xffff; + uint32_t s2 = adler >> 16; + + /* + * Process the data in blocks. + */ + const unsigned BLOCK_SIZE = 1 << 5; + + z_size_t blocks = len / BLOCK_SIZE; + len -= blocks * BLOCK_SIZE; + + while (blocks) + { + unsigned n = NMAX / BLOCK_SIZE; /* The NMAX constraint. */ + if (n > blocks) + n = (unsigned) blocks; + blocks -= n; + + const __m128i tap1 = + _mm_setr_epi8(32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17); + const __m128i tap2 = + _mm_setr_epi8(16,15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1); + const __m128i zero = + _mm_setr_epi8( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + const __m128i ones = + _mm_set_epi16( 1, 1, 1, 1, 1, 1, 1, 1); + + /* + * Process n blocks of data. At most NMAX data bytes can be + * processed before s2 must be reduced modulo BASE. + */ + __m128i v_ps = _mm_set_epi32(0, 0, 0, s1 * n); + __m128i v_s2 = _mm_set_epi32(0, 0, 0, s2); + __m128i v_s1 = _mm_set_epi32(0, 0, 0, 0); + + do { + /* + * Load 32 input bytes. + */ + const __m128i bytes1 = _mm_loadu_si128((__m128i*)(buf)); + const __m128i bytes2 = _mm_loadu_si128((__m128i*)(buf + 16)); + + /* + * Add previous block byte sum to v_ps. + */ + v_ps = _mm_add_epi32(v_ps, v_s1); + + /* + * Horizontally add the bytes for s1, multiply-adds the + * bytes by [ 32, 31, 30, ... ] for s2. + */ + v_s1 = _mm_add_epi32(v_s1, _mm_sad_epu8(bytes1, zero)); + const __m128i mad1 = _mm_maddubs_epi16(bytes1, tap1); + v_s2 = _mm_add_epi32(v_s2, _mm_madd_epi16(mad1, ones)); + + v_s1 = _mm_add_epi32(v_s1, _mm_sad_epu8(bytes2, zero)); + const __m128i mad2 = _mm_maddubs_epi16(bytes2, tap2); + v_s2 = _mm_add_epi32(v_s2, _mm_madd_epi16(mad2, ones)); + + buf += BLOCK_SIZE; + + } while (--n); + + v_s2 = _mm_add_epi32(v_s2, _mm_slli_epi32(v_ps, 5)); + + /* + * Sum epi32 ints v_s1(s2) and accumulate in s1(s2). + */ + +#define S23O1 _MM_SHUFFLE(2,3,0,1) /* A B C D -> B A D C */ +#define S1O32 _MM_SHUFFLE(1,0,3,2) /* A B C D -> C D A B */ + + v_s1 = _mm_add_epi32(v_s1, _mm_shuffle_epi32(v_s1, S23O1)); + v_s1 = _mm_add_epi32(v_s1, _mm_shuffle_epi32(v_s1, S1O32)); + + s1 += _mm_cvtsi128_si32(v_s1); + + v_s2 = _mm_add_epi32(v_s2, _mm_shuffle_epi32(v_s2, S23O1)); + v_s2 = _mm_add_epi32(v_s2, _mm_shuffle_epi32(v_s2, S1O32)); + + s2 = _mm_cvtsi128_si32(v_s2); + +#undef S23O1 +#undef S1O32 + + /* + * Reduce. + */ + s1 %= BASE; + s2 %= BASE; + } + + /* + * Handle leftover data. + */ + if (len) { + if (len >= 16) { + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + len -= 16; + } + + while (len--) { + s2 += (s1 += *buf++); + } + + if (s1 >= BASE) + s1 -= BASE; + s2 %= BASE; + } + + /* + * Return the recombined sums. + */ + return s1 | (s2 << 16); +} + +#elif defined(ADLER32_SIMD_NEON) + +#include + +uint32_t ZLIB_INTERNAL adler32_simd_( /* NEON */ + uint32_t adler, + const unsigned char *buf, + z_size_t len) +{ + /* + * Split Adler-32 into component sums. + */ + uint32_t s1 = adler & 0xffff; + uint32_t s2 = adler >> 16; + + /* + * Serially compute s1 & s2, until the data is 16-byte aligned. + */ + if ((uintptr_t)buf & 15) { + while ((uintptr_t)buf & 15) { + s2 += (s1 += *buf++); + --len; + } + + if (s1 >= BASE) + s1 -= BASE; + s2 %= BASE; + } + + /* + * Process the data in blocks. + */ + const unsigned BLOCK_SIZE = 1 << 5; + + z_size_t blocks = len / BLOCK_SIZE; + len -= blocks * BLOCK_SIZE; + + while (blocks) + { + unsigned n = NMAX / BLOCK_SIZE; /* The NMAX constraint. */ + if (n > blocks) + n = (unsigned) blocks; + blocks -= n; + + /* + * Process n blocks of data. At most NMAX data bytes can be + * processed before s2 must be reduced modulo BASE. + */ + uint32x4_t v_s2 = (uint32x4_t) { 0, 0, 0, s1 * n }; + uint32x4_t v_s1 = (uint32x4_t) { 0, 0, 0, 0 }; + + uint16x8_t v_column_sum_1 = vdupq_n_u16(0); + uint16x8_t v_column_sum_2 = vdupq_n_u16(0); + uint16x8_t v_column_sum_3 = vdupq_n_u16(0); + uint16x8_t v_column_sum_4 = vdupq_n_u16(0); + + do { + /* + * Load 32 input bytes. + */ + const uint8x16_t bytes1 = vld1q_u8((uint8_t*)(buf)); + const uint8x16_t bytes2 = vld1q_u8((uint8_t*)(buf + 16)); + + /* + * Add previous block byte sum to v_s2. + */ + v_s2 = vaddq_u32(v_s2, v_s1); + + /* + * Horizontally add the bytes for s1. + */ + v_s1 = vpadalq_u16(v_s1, vpadalq_u8(vpaddlq_u8(bytes1), bytes2)); + + /* + * Vertically add the bytes for s2. + */ + v_column_sum_1 = vaddw_u8(v_column_sum_1, vget_low_u8 (bytes1)); + v_column_sum_2 = vaddw_u8(v_column_sum_2, vget_high_u8(bytes1)); + v_column_sum_3 = vaddw_u8(v_column_sum_3, vget_low_u8 (bytes2)); + v_column_sum_4 = vaddw_u8(v_column_sum_4, vget_high_u8(bytes2)); + + buf += BLOCK_SIZE; + + } while (--n); + + v_s2 = vshlq_n_u32(v_s2, 5); + + /* + * Multiply-add bytes by [ 32, 31, 30, ... ] for s2. + */ + v_s2 = vmlal_u16(v_s2, vget_low_u16 (v_column_sum_1), + (uint16x4_t) { 32, 31, 30, 29 }); + v_s2 = vmlal_u16(v_s2, vget_high_u16(v_column_sum_1), + (uint16x4_t) { 28, 27, 26, 25 }); + v_s2 = vmlal_u16(v_s2, vget_low_u16 (v_column_sum_2), + (uint16x4_t) { 24, 23, 22, 21 }); + v_s2 = vmlal_u16(v_s2, vget_high_u16(v_column_sum_2), + (uint16x4_t) { 20, 19, 18, 17 }); + v_s2 = vmlal_u16(v_s2, vget_low_u16 (v_column_sum_3), + (uint16x4_t) { 16, 15, 14, 13 }); + v_s2 = vmlal_u16(v_s2, vget_high_u16(v_column_sum_3), + (uint16x4_t) { 12, 11, 10, 9 }); + v_s2 = vmlal_u16(v_s2, vget_low_u16 (v_column_sum_4), + (uint16x4_t) { 8, 7, 6, 5 }); + v_s2 = vmlal_u16(v_s2, vget_high_u16(v_column_sum_4), + (uint16x4_t) { 4, 3, 2, 1 }); + + /* + * Sum epi32 ints v_s1(s2) and accumulate in s1(s2). + */ + uint32x2_t sum1 = vpadd_u32(vget_low_u32(v_s1), vget_high_u32(v_s1)); + uint32x2_t sum2 = vpadd_u32(vget_low_u32(v_s2), vget_high_u32(v_s2)); + uint32x2_t s1s2 = vpadd_u32(sum1, sum2); + + s1 += vget_lane_u32(s1s2, 0); + s2 += vget_lane_u32(s1s2, 1); + + /* + * Reduce. + */ + s1 %= BASE; + s2 %= BASE; + } + + /* + * Handle leftover data. + */ + if (len) { + if (len >= 16) { + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + s2 += (s1 += *buf++); + + len -= 16; + } + + while (len--) { + s2 += (s1 += *buf++); + } + + if (s1 >= BASE) + s1 -= BASE; + s2 %= BASE; + } + + /* + * Return the recombined sums. + */ + return s1 | (s2 << 16); +} + +#elif defined(ADLER32_SIMD_RVV) +#include + +/* + * Patch by Simon Hosie, from: + * https://github.com/cloudflare/zlib/pull/55 + */ + +uint32_t ZLIB_INTERNAL adler32_simd_( /* RVV */ + uint32_t adler, + const unsigned char *buf, + unsigned long len) +{ + size_t vl = __riscv_vsetvlmax_e8m2(); + const vuint16m4_t zero16 = __riscv_vmv_v_x_u16m4(0, vl); + vuint16m4_t a_sum = zero16; + vuint32m8_t b_sum = __riscv_vmv_v_x_u32m8(0, vl); + + /* Deal with the part which is not a multiple of vl first; because it's + * easier to zero-stuff the beginning of the checksum than it is to tweak the + * multipliers and sums for odd lengths afterwards. + */ + size_t head = len & (vl - 1); + if (head > 0) { + vuint8m2_t zero8 = __riscv_vmv_v_x_u8m2(0, vl); + vuint8m2_t in = __riscv_vle8_v_u8m2(buf, vl); + in = __riscv_vslideup(zero8, in, vl - head, vl); + vuint16m4_t in16 = __riscv_vwcvtu_x(in, vl); + a_sum = in16; + buf += head; + } + + /* We have a 32-bit accumulator, and in each iteration we add 22-times a + * 16-bit value, plus another 16-bit value. We periodically subtract up to + * 65535 times BASE to avoid overflow. b_overflow estimates how often we + * need to do this subtraction. + */ + const int b_overflow = BASE / 23; + int fixup = b_overflow; + ssize_t iters = (len - head) / vl; + while (iters > 0) { + const vuint16m4_t a_overflow = __riscv_vrsub(a_sum, BASE, vl); + int batch = iters < 22 ? iters : 22; + iters -= batch; + b_sum = __riscv_vwmaccu(b_sum, batch, a_sum, vl); + vuint16m4_t a_batch = zero16, b_batch = zero16; + + /* Do a short batch, where neither a_sum nor b_sum can overflow a 16-bit + * register. Then add them back into the main accumulators. + */ + while (batch-- > 0) { + vuint8m2_t in8 = __riscv_vle8_v_u8m2(buf, vl); + buf += vl; + b_batch = __riscv_vadd(b_batch, a_batch, vl); + a_batch = __riscv_vwaddu_wv(a_batch, in8, vl); + } + vbool4_t ov = __riscv_vmsgeu(a_batch, a_overflow, vl); + a_sum = __riscv_vadd(a_sum, a_batch, vl); + a_sum = __riscv_vadd_mu(ov, a_sum, a_sum, 65536 - BASE, vl); + b_sum = __riscv_vwaddu_wv(b_sum, b_batch, vl); + if (--fixup <= 0) { + b_sum = __riscv_vnmsac(b_sum, BASE, __riscv_vsrl(b_sum, 16, vl), vl); + fixup = b_overflow; + } + } + /* Adjust per-lane sums to have appropriate offsets from the end of the + * buffer. + */ + const vuint16m4_t off = __riscv_vrsub(__riscv_vid_v_u16m4(vl), vl, vl); + vuint16m4_t bsum16 = __riscv_vncvt_x(__riscv_vremu(b_sum, BASE, vl), vl); + b_sum = __riscv_vadd(__riscv_vwmulu(a_sum, off, vl), + __riscv_vwmulu(bsum16, vl, vl), vl); + bsum16 = __riscv_vncvt_x(__riscv_vremu(b_sum, BASE, vl), vl); + + /* And finally, do a horizontal sum across the registers for the final + * result. + */ + uint32_t a = adler & 0xffff; + uint32_t b = ((adler >> 16) + a * (len % BASE)) % BASE; + vuint32m1_t sca = __riscv_vmv_v_x_u32m1(a, 1); + vuint32m1_t scb = __riscv_vmv_v_x_u32m1(b, 1); + sca = __riscv_vwredsumu(a_sum, sca, vl); + scb = __riscv_vwredsumu(bsum16, scb, vl); + a = __riscv_vmv_x(sca); + b = __riscv_vmv_x(scb); + a %= BASE; + b %= BASE; + return (b << 16) | a; +} + +#endif /* ADLER32_SIMD_SSSE3 */ diff --git a/packages/runtime/vendor/zlib/adler32_simd.h b/packages/runtime/vendor/zlib/adler32_simd.h new file mode 100644 index 000000000..0b2361a51 --- /dev/null +++ b/packages/runtime/vendor/zlib/adler32_simd.h @@ -0,0 +1,16 @@ +/* adler32_simd.h + * + * Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#include + +#include "zconf.h" +#include "zutil.h" + +uint32_t ZLIB_INTERNAL adler32_simd_( + uint32_t adler, + const unsigned char *buf, + z_size_t len); diff --git a/packages/runtime/vendor/zlib/chromeconf.h b/packages/runtime/vendor/zlib/chromeconf.h new file mode 100644 index 000000000..77f0e29e0 --- /dev/null +++ b/packages/runtime/vendor/zlib/chromeconf.h @@ -0,0 +1,211 @@ +/* Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#ifndef THIRD_PARTY_ZLIB_CHROMECONF_H_ +#define THIRD_PARTY_ZLIB_CHROMECONF_H_ + +#if defined(COMPONENT_BUILD) +#if defined(WIN32) +#if defined(ZLIB_IMPLEMENTATION) +#define ZEXTERN __declspec(dllexport) +#else +#define ZEXTERN __declspec(dllimport) +#endif +#elif defined(ZLIB_IMPLEMENTATION) +#define ZEXTERN __attribute__((visibility("default"))) +#endif +#endif + +/* Rename all zlib names with a Cr_z_ prefix. This is based on the Z_PREFIX + * option from zconf.h, but with a custom prefix. Where zconf.h would rename + * both a macro and its underscore-suffixed internal implementation (such as + * deflateInit2 and deflateInit2_), only the implementation is renamed here. + * The Byte type is also omitted. + * + * To generate this list, run + * sed -rn -e 's/^# *define +([^ ]+) +(z_[^ ]+)$/#define \1 Cr_\2/p' zconf.h + * (use -E instead of -r on macOS). + * + * gzread is also addressed by modifications in gzread.c and zlib.h. */ + +#define Z_CR_PREFIX_SET + +#define _dist_code Cr_z__dist_code +#define _length_code Cr_z__length_code +#define _tr_align Cr_z__tr_align +#define _tr_flush_bits Cr_z__tr_flush_bits +#define _tr_flush_block Cr_z__tr_flush_block +#define _tr_init Cr_z__tr_init +#define _tr_stored_block Cr_z__tr_stored_block +#define _tr_tally Cr_z__tr_tally +#define adler32 Cr_z_adler32 +#define adler32_combine Cr_z_adler32_combine +#define adler32_combine64 Cr_z_adler32_combine64 +#define adler32_z Cr_z_adler32_z +#define compress Cr_z_compress +#define compress2 Cr_z_compress2 +#define compress_z Cr_z_compress_z +#define compress2_z Cr_z_compress2_z +#define compressBound Cr_z_compressBound +#define compressBound_z Cr_z_compressBound_z +#define crc32 Cr_z_crc32 +#define crc32_combine Cr_z_crc32_combine +#define crc32_combine64 Cr_z_crc32_combine64 +#define crc32_combine_gen Cr_z_crc32_combine_gen +#define crc32_combine_gen64 Cr_z_crc32_combine_gen64 +#define crc32_combine_op Cr_z_crc32_combine_op +#define crc32_z Cr_z_crc32_z +#define deflate Cr_z_deflate +#define deflateBound Cr_z_deflateBound +#define deflateBound_z Cr_z_deflateBound_z +#define deflateCopy Cr_z_deflateCopy +#define deflateEnd Cr_z_deflateEnd +#define deflateGetDictionary Cr_z_deflateGetDictionary +/* #undef deflateInit */ +/* #undef deflateInit2 */ +#define deflateInit2_ Cr_z_deflateInit2_ +#define deflateInit_ Cr_z_deflateInit_ +#define deflateParams Cr_z_deflateParams +#define deflatePending Cr_z_deflatePending +#define deflatePrime Cr_z_deflatePrime +#define deflateReset Cr_z_deflateReset +#define deflateResetKeep Cr_z_deflateResetKeep +#define deflateSetDictionary Cr_z_deflateSetDictionary +#define deflateSetHeader Cr_z_deflateSetHeader +#define deflateTune Cr_z_deflateTune +#define deflateUsed Cr_z_deflateUsed +#define deflate_copyright Cr_z_deflate_copyright +#define get_crc_table Cr_z_get_crc_table +#define gz_error Cr_z_gz_error +#define gz_intmax Cr_z_gz_intmax +#define gz_strwinerror Cr_z_gz_strwinerror +#define gzbuffer Cr_z_gzbuffer +#define gzclearerr Cr_z_gzclearerr +#define gzclose Cr_z_gzclose +#define gzclose_r Cr_z_gzclose_r +#define gzclose_w Cr_z_gzclose_w +#define gzdirect Cr_z_gzdirect +#define gzdopen Cr_z_gzdopen +#define gzeof Cr_z_gzeof +#define gzerror Cr_z_gzerror +#define gzflush Cr_z_gzflush +#define gzfread Cr_z_gzfread +#define gzfwrite Cr_z_gzfwrite +#define gzgetc Cr_z_gzgetc +#define gzgetc_ Cr_z_gzgetc_ +#define gzgets Cr_z_gzgets +#define gzoffset Cr_z_gzoffset +#define gzoffset64 Cr_z_gzoffset64 +#define gzopen Cr_z_gzopen +#define gzopen64 Cr_z_gzopen64 +#define gzopen_w Cr_z_gzopen_w +#define gzprintf Cr_z_gzprintf +#define gzputc Cr_z_gzputc +#define gzputs Cr_z_gzputs +#define gzread Cr_z_gzread +#define gzrewind Cr_z_gzrewind +#define gzseek Cr_z_gzseek +#define gzseek64 Cr_z_gzseek64 +#define gzsetparams Cr_z_gzsetparams +#define gztell Cr_z_gztell +#define gztell64 Cr_z_gztell64 +#define gzungetc Cr_z_gzungetc +#define gzvprintf Cr_z_gzvprintf +#define gzwrite Cr_z_gzwrite +#define inflate Cr_z_inflate +#define inflateBack Cr_z_inflateBack +#define inflateBackEnd Cr_z_inflateBackEnd +/* #undef inflateBackInit */ +#define inflateBackInit_ Cr_z_inflateBackInit_ +#define inflateCodesUsed Cr_z_inflateCodesUsed +#define inflateCopy Cr_z_inflateCopy +#define inflateEnd Cr_z_inflateEnd +#define inflateGetDictionary Cr_z_inflateGetDictionary +#define inflateGetHeader Cr_z_inflateGetHeader +/* #undef inflateInit */ +/* #undef inflateInit2 */ +#define inflateInit2_ Cr_z_inflateInit2_ +#define inflateInit_ Cr_z_inflateInit_ +#define inflateMark Cr_z_inflateMark +#define inflatePrime Cr_z_inflatePrime +#define inflateReset Cr_z_inflateReset +#define inflateReset2 Cr_z_inflateReset2 +#define inflateResetKeep Cr_z_inflateResetKeep +#define inflateSetDictionary Cr_z_inflateSetDictionary +#define inflateSync Cr_z_inflateSync +#define inflateSyncPoint Cr_z_inflateSyncPoint +#define inflateUndermine Cr_z_inflateUndermine +#define inflateValidate Cr_z_inflateValidate +#define inflate_copyright Cr_z_inflate_copyright +#define inflate_fast Cr_z_inflate_fast +#define inflate_table Cr_z_inflate_table +#define inflate_fixed Cr_z_inflate_fixed +#define uncompress Cr_z_uncompress +#define uncompress2 Cr_z_uncompress2 +#define uncompress_z Cr_z_uncompress_z +#define uncompress2_z Cr_z_uncompress2_z +#define zError Cr_z_zError +#define zcalloc Cr_z_zcalloc +#define zcfree Cr_z_zcfree +#define zlibCompileFlags Cr_z_zlibCompileFlags +#define zlibVersion Cr_z_zlibVersion +/* #undef Byte */ +#define Bytef Cr_z_Bytef +#define alloc_func Cr_z_alloc_func +#define charf Cr_z_charf +#define free_func Cr_z_free_func +#define gzFile Cr_z_gzFile +#define gz_header Cr_z_gz_header +#define gz_headerp Cr_z_gz_headerp +#define in_func Cr_z_in_func +#define intf Cr_z_intf +#define out_func Cr_z_out_func +#define uInt Cr_z_uInt +#define uIntf Cr_z_uIntf +#define uLong Cr_z_uLong +#define uLongf Cr_z_uLongf +#define voidp Cr_z_voidp +#define voidpc Cr_z_voidpc +#define voidpf Cr_z_voidpf +#define gz_header_s Cr_z_gz_header_s +/* #undef internal_state */ +/* #undef z_off64_t */ + +/* An exported symbol that isn't handled by Z_PREFIX in zconf.h */ +#define z_errmsg Cr_z_z_errmsg + +/* Symbols added in simd.patch */ +#define copy_with_crc Cr_z_copy_with_crc +#define crc_finalize Cr_z_crc_finalize +#define crc_fold_512to32 Cr_z_crc_fold_512to32 +#define crc_fold_copy Cr_z_crc_fold_copy +#define crc_fold_init Cr_z_crc_fold_init +#define crc_reset Cr_z_crc_reset +#define fill_window_sse Cr_z_fill_window_sse +#define deflate_read_buf Cr_z_deflate_read_buf +#define x86_check_features Cr_z_x86_check_features +#define x86_cpu_enable_simd Cr_z_x86_cpu_enable_simd + +/* Symbols added by adler_simd.c */ +#define adler32_simd_ Cr_z_adler32_simd_ +#define x86_cpu_enable_ssse3 Cr_z_x86_cpu_enable_ssse3 + +/* Symbols added by contrib/optimizations/inffast_chunk */ +#define inflate_fast_chunk_ Cr_z_inflate_fast_chunk_ + +/* Symbols added by crc32_simd.c */ +#define crc32_sse42_simd_ Cr_z_crc32_sse42_simd_ + +/* Symbols added by armv8_crc32 */ +#define arm_cpu_enable_crc32 Cr_z_arm_cpu_enable_crc32 +#define arm_cpu_enable_pmull Cr_z_arm_cpu_enable_pmull +#define arm_check_features Cr_z_arm_check_features +#define armv8_crc32_little Cr_z_armv8_crc32_little +#define armv8_crc32_pmull_little Cr_z_armv8_crc32_pmull_little + +/* Symbols added by cpu_features.c */ +#define cpu_check_features Cr_z_cpu_check_features +#define x86_cpu_enable_sse2 Cr_z_x86_cpu_enable_sse2 + +#endif /* THIRD_PARTY_ZLIB_CHROMECONF_H_ */ diff --git a/packages/runtime/vendor/zlib/compare256.h b/packages/runtime/vendor/zlib/compare256.h new file mode 100644 index 000000000..80061c3fe --- /dev/null +++ b/packages/runtime/vendor/zlib/compare256.h @@ -0,0 +1,56 @@ +/* compare256.h + * + * This does 256-byte match comparison for deflate's longest_match. + * + * Copyright 2026 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ +#ifndef COMPARE256_H +#define COMPARE256_H + +#include +#include +#if defined(_MSC_VER) && !defined(__clang__) +#include +#endif + +/* Safe unaligned 16-bit load. Compilers optimize this into a single load + * instruction. */ +static inline uint16_t read16(const void* p) { + uint16_t v; + memcpy(&v, p, sizeof(v)); + return v; +} + +/* Finds the byte offset (0..7) of the first difference in non-zero XOR mask + * `x`: + */ +static inline int compare256_diff(uint64_t x) { +#if defined(_MSC_VER) && !defined(__clang__) + unsigned long i; + _BitScanForward64(&i, x); + return (int)i / 8; +#else + return __builtin_ctzll(x) / 8; +#endif +} + +/* Returns the number of matching leading bytes (0 to 256) between src0 and + * src1. Compares 8 bytes per iteration with early exit. */ +static inline int compare256(const unsigned char* src0, + const unsigned char* src1) { + int len = 0; + do { + uint64_t a, b, x; + memcpy(&a, src0 + len, sizeof(a)); + memcpy(&b, src1 + len, sizeof(b)); + x = a ^ b; + if (x) + return len + compare256_diff(x); + len += (int)sizeof(a); + } while (len < 256); + return 256; +} + +#endif /* COMPARE256_H */ diff --git a/packages/runtime/vendor/zlib/compress.c b/packages/runtime/vendor/zlib/compress.c index f43bacf7a..e3a7516e4 100644 --- a/packages/runtime/vendor/zlib/compress.c +++ b/packages/runtime/vendor/zlib/compress.c @@ -1,5 +1,5 @@ /* compress.c -- compress a memory buffer - * Copyright (C) 1995-2005, 2014, 2016 Jean-loup Gailly, Mark Adler + * Copyright (C) 1995-2026 Jean-loup Gailly, Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -18,13 +18,19 @@ compress2 returns Z_OK if success, Z_MEM_ERROR if there was not enough memory, Z_BUF_ERROR if there was not enough room in the output buffer, Z_STREAM_ERROR if the level parameter is invalid. + + The _z versions of the functions take size_t length arguments. */ -int ZEXPORT compress2(Bytef *dest, uLongf *destLen, const Bytef *source, - uLong sourceLen, int level) { +int ZEXPORT compress2_z(Bytef *dest, z_size_t *destLen, const Bytef *source, + z_size_t sourceLen, int level) { z_stream stream; int err; const uInt max = (uInt)-1; - uLong left; + z_size_t left; + + if ((sourceLen > 0 && source == NULL) || + destLen == NULL || (*destLen > 0 && dest == NULL)) + return Z_STREAM_ERROR; left = *destLen; *destLen = 0; @@ -43,23 +49,36 @@ int ZEXPORT compress2(Bytef *dest, uLongf *destLen, const Bytef *source, do { if (stream.avail_out == 0) { - stream.avail_out = left > (uLong)max ? max : (uInt)left; + stream.avail_out = left > (z_size_t)max ? max : (uInt)left; left -= stream.avail_out; } if (stream.avail_in == 0) { - stream.avail_in = sourceLen > (uLong)max ? max : (uInt)sourceLen; + stream.avail_in = sourceLen > (z_size_t)max ? max : + (uInt)sourceLen; sourceLen -= stream.avail_in; } err = deflate(&stream, sourceLen ? Z_NO_FLUSH : Z_FINISH); } while (err == Z_OK); - *destLen = stream.total_out; + *destLen = (z_size_t)(stream.next_out - dest); deflateEnd(&stream); return err == Z_STREAM_END ? Z_OK : err; } - +int ZEXPORT compress2(Bytef *dest, uLongf *destLen, const Bytef *source, + uLong sourceLen, int level) { + int ret; + z_size_t got = *destLen; + ret = compress2_z(dest, &got, source, sourceLen, level); + *destLen = (uLong)got; + return ret; +} /* =========================================================================== */ +int ZEXPORT compress_z(Bytef *dest, z_size_t *destLen, const Bytef *source, + z_size_t sourceLen) { + return compress2_z(dest, destLen, source, sourceLen, + Z_DEFAULT_COMPRESSION); +} int ZEXPORT compress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen) { return compress2(dest, destLen, source, sourceLen, Z_DEFAULT_COMPRESSION); @@ -69,7 +88,23 @@ int ZEXPORT compress(Bytef *dest, uLongf *destLen, const Bytef *source, If the default memLevel or windowBits for deflateInit() is changed, then this function needs to be updated. */ +z_size_t ZEXPORT compressBound_z(z_size_t sourceLen) { + z_size_t bound = sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + + (sourceLen >> 25) + 13; + + /* FIXME(cavalcantii): usage of CRC32 Castagnoli as a hash function + * for the hash table of symbols used for compression has a side effect + * where for compression level [4, 5] it will increase the output buffer size + * by 0.1% (i.e. less than 1%) for a high entropy input (i.e. random data). + * To avoid a scenario where client code would fail, for safety we increase + * the expected output size by 0.8% (i.e. 8x more than the worst scenario). + * See: http://crbug.com/990489 + */ + bound += bound >> 7; // Equivalent to 1.0078125 + + return bound < sourceLen ? (z_size_t)-1 : bound; +} uLong ZEXPORT compressBound(uLong sourceLen) { - return sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + - (sourceLen >> 25) + 13; + z_size_t bound = compressBound_z(sourceLen); + return (uLong)bound != bound ? (uLong)-1 : (uLong)bound; } diff --git a/packages/runtime/vendor/zlib/contrib/optimizations/chunkcopy.h b/packages/runtime/vendor/zlib/contrib/optimizations/chunkcopy.h new file mode 100644 index 000000000..97efff3a4 --- /dev/null +++ b/packages/runtime/vendor/zlib/contrib/optimizations/chunkcopy.h @@ -0,0 +1,564 @@ +/* chunkcopy.h -- fast chunk copy and set operations + * Copyright (C) 2017 ARM, Inc. + * Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#ifndef CHUNKCOPY_H +#define CHUNKCOPY_H + +#include +#include "zutil.h" + +#define Z_STATIC_ASSERT(name, assert) typedef char name[(assert) ? 1 : -1] + +#if __STDC_VERSION__ >= 199901L +#define Z_RESTRICT restrict +#else +#define Z_RESTRICT +#endif + +#if defined(__clang__) || defined(__GNUC__) || defined(__llvm__) +#define Z_BUILTIN_MEMCPY __builtin_memcpy +#define Z_BUILTIN_MEMSET __builtin_memset +#else +#define Z_BUILTIN_MEMCPY zmemcpy +#define Z_BUILTIN_MEMSET zmemset +#endif + +#if defined(INFLATE_CHUNK_SIMD_NEON) +#include +typedef uint8x16_t z_vec128i_t; +#elif defined(INFLATE_CHUNK_SIMD_SSE2) +#include +typedef __m128i z_vec128i_t; +#elif defined(INFLATE_CHUNK_GENERIC) +typedef struct { uint8_t x[16]; } z_vec128i_t; +#else +#error chunkcopy.h inflate chunk SIMD is not defined for your build target +#endif + +/* + * Suppress MSan errors about copying uninitialized bytes (crbug.com/1376033). + */ +#define Z_DISABLE_MSAN +#if defined(__has_feature) + #if __has_feature(memory_sanitizer) + #undef Z_DISABLE_MSAN + #define Z_DISABLE_MSAN __attribute__((no_sanitize("memory"))) + #endif +#endif + +/* + * chunk copy type: the z_vec128i_t type size should be exactly 128-bits + * and equal to CHUNKCOPY_CHUNK_SIZE. + */ +#define CHUNKCOPY_CHUNK_SIZE sizeof(z_vec128i_t) + +Z_STATIC_ASSERT(vector_128_bits_wide, + CHUNKCOPY_CHUNK_SIZE == sizeof(int8_t) * 16); + +/* + * Ask the compiler to perform a wide, unaligned load with a machine + * instruction appropriate for the z_vec128i_t type. + */ +static inline z_vec128i_t loadchunk( + const unsigned char FAR* s) Z_DISABLE_MSAN { + z_vec128i_t v; + Z_BUILTIN_MEMCPY(&v, s, sizeof(v)); + return v; +} + +/* + * Ask the compiler to perform a wide, unaligned store with a machine + * instruction appropriate for the z_vec128i_t type. + */ +static inline void storechunk( + unsigned char FAR* d, + const z_vec128i_t v) { + Z_BUILTIN_MEMCPY(d, &v, sizeof(v)); +} + +/* + * Perform a memcpy-like operation, assuming that length is non-zero and that + * it's OK to overwrite at least CHUNKCOPY_CHUNK_SIZE bytes of output even if + * the length is shorter than this. + * + * It also guarantees that it will properly unroll the data if the distance + * between `out` and `from` is at least CHUNKCOPY_CHUNK_SIZE, which we rely on + * in chunkcopy_relaxed(). + * + * Aside from better memory bus utilisation, this means that short copies + * (CHUNKCOPY_CHUNK_SIZE bytes or fewer) will fall straight through the loop + * without iteration, which will hopefully make the branch prediction more + * reliable. + */ +static inline unsigned char FAR* chunkcopy_core( + unsigned char FAR* out, + const unsigned char FAR* from, + unsigned len) Z_DISABLE_MSAN { + const int bump = (--len % CHUNKCOPY_CHUNK_SIZE) + 1; + storechunk(out, loadchunk(from)); + out += bump; + from += bump; + len /= CHUNKCOPY_CHUNK_SIZE; + while (len-- > 0) { + storechunk(out, loadchunk(from)); + out += CHUNKCOPY_CHUNK_SIZE; + from += CHUNKCOPY_CHUNK_SIZE; + } + return out; +} + +/* + * Like chunkcopy_core(), but avoid writing beyond of legal output. + * + * Accepts an additional pointer to the end of safe output. A generic safe + * copy would use (out + len), but it's normally the case that the end of the + * output buffer is beyond the end of the current copy, and this can still be + * exploited. + */ +static inline unsigned char FAR* chunkcopy_core_safe( + unsigned char FAR* out, + const unsigned char FAR* from, + unsigned len, + unsigned char FAR* limit) { + Assert(out + len <= limit, "chunk copy exceeds safety limit"); + if ((limit - out) < (ptrdiff_t)CHUNKCOPY_CHUNK_SIZE) { + const unsigned char FAR* Z_RESTRICT rfrom = from; + Assert((uintptr_t)out - (uintptr_t)from >= len, + "invalid restrict in chunkcopy_core_safe"); + Assert((uintptr_t)from - (uintptr_t)out >= len, + "invalid restrict in chunkcopy_core_safe"); + if (len & 8) { + Z_BUILTIN_MEMCPY(out, rfrom, 8); + out += 8; + rfrom += 8; + } + if (len & 4) { + Z_BUILTIN_MEMCPY(out, rfrom, 4); + out += 4; + rfrom += 4; + } + if (len & 2) { + Z_BUILTIN_MEMCPY(out, rfrom, 2); + out += 2; + rfrom += 2; + } + if (len & 1) { + *out++ = *rfrom++; + } + return out; + } + return chunkcopy_core(out, from, len); +} + +/* + * Perform short copies until distance can be rewritten as being at least + * CHUNKCOPY_CHUNK_SIZE. + * + * Assumes it's OK to overwrite at least the first 2*CHUNKCOPY_CHUNK_SIZE + * bytes of output even if the copy is shorter than this. This assumption + * holds within zlib inflate_fast(), which starts every iteration with at + * least 258 bytes of output space available (258 being the maximum length + * output from a single token; see inffast.c). + */ +static inline unsigned char FAR* chunkunroll_relaxed( + unsigned char FAR* out, + unsigned FAR* dist, + unsigned FAR* len) Z_DISABLE_MSAN { + const unsigned char FAR* from = out - *dist; + while (*dist < *len && *dist < CHUNKCOPY_CHUNK_SIZE) { + storechunk(out, loadchunk(from)); + out += *dist; + *len -= *dist; + *dist += *dist; + } + return out; +} + +#if defined(INFLATE_CHUNK_SIMD_NEON) +/* + * v_load64_dup(): load *src as an unaligned 64-bit int and duplicate it in + * every 64-bit component of the 128-bit result (64-bit int splat). + */ +static inline z_vec128i_t v_load64_dup(const void* src) { + return vcombine_u8(vld1_u8(src), vld1_u8(src)); +} + +/* + * v_load32_dup(): load *src as an unaligned 32-bit int and duplicate it in + * every 32-bit component of the 128-bit result (32-bit int splat). + */ +static inline z_vec128i_t v_load32_dup(const void* src) { + int32_t i32; + Z_BUILTIN_MEMCPY(&i32, src, sizeof(i32)); + return vreinterpretq_u8_s32(vdupq_n_s32(i32)); +} + +/* + * v_load16_dup(): load *src as an unaligned 16-bit int and duplicate it in + * every 16-bit component of the 128-bit result (16-bit int splat). + */ +static inline z_vec128i_t v_load16_dup(const void* src) { + int16_t i16; + Z_BUILTIN_MEMCPY(&i16, src, sizeof(i16)); + return vreinterpretq_u8_s16(vdupq_n_s16(i16)); +} + +/* + * v_load8_dup(): load the 8-bit int *src and duplicate it in every 8-bit + * component of the 128-bit result (8-bit int splat). + */ +static inline z_vec128i_t v_load8_dup(const void* src) { + return vld1q_dup_u8((const uint8_t*)src); +} + +/* + * v_store_128(): store the 128-bit vec in a memory destination (that might + * not be 16-byte aligned) void* out. + */ +static inline void v_store_128(void* out, const z_vec128i_t vec) { + vst1q_u8(out, vec); +} + +#elif defined(INFLATE_CHUNK_SIMD_SSE2) +/* + * v_load64_dup(): load *src as an unaligned 64-bit int and duplicate it in + * every 64-bit component of the 128-bit result (64-bit int splat). + */ +static inline z_vec128i_t v_load64_dup(const void* src) { + int64_t i64; + Z_BUILTIN_MEMCPY(&i64, src, sizeof(i64)); + return _mm_set1_epi64x(i64); +} + +/* + * v_load32_dup(): load *src as an unaligned 32-bit int and duplicate it in + * every 32-bit component of the 128-bit result (32-bit int splat). + */ +static inline z_vec128i_t v_load32_dup(const void* src) { + int32_t i32; + Z_BUILTIN_MEMCPY(&i32, src, sizeof(i32)); + return _mm_set1_epi32(i32); +} + +/* + * v_load16_dup(): load *src as an unaligned 16-bit int and duplicate it in + * every 16-bit component of the 128-bit result (16-bit int splat). + */ +static inline z_vec128i_t v_load16_dup(const void* src) { + int16_t i16; + Z_BUILTIN_MEMCPY(&i16, src, sizeof(i16)); + return _mm_set1_epi16(i16); +} + +/* + * v_load8_dup(): load the 8-bit int *src and duplicate it in every 8-bit + * component of the 128-bit result (8-bit int splat). + */ +static inline z_vec128i_t v_load8_dup(const void* src) { + return _mm_set1_epi8(*(const char*)src); +} + +/* + * v_store_128(): store the 128-bit vec in a memory destination (that might + * not be 16-byte aligned) void* out. + */ +static inline void v_store_128(void* out, const z_vec128i_t vec) { + _mm_storeu_si128((__m128i*)out, vec); +} +#elif defined(INFLATE_CHUNK_GENERIC) +/* + * Default implementations for chunk-copy functions rely on memcpy() being + * inlined by the compiler for best performance. This is most likely to work + * as expected when the length argument is constant (as is the case here) and + * the target supports unaligned loads and stores. Since that's not always a + * safe assumption, this may need extra compiler arguments such as + * `-mno-strict-align` or `-munaligned-access`, or the availability of + * extensions like SIMD. + */ + +/* + * v_load64_dup(): load *src as an unaligned 64-bit int and duplicate it in + * every 64-bit component of the 128-bit result (64-bit int splat). + */ +static inline z_vec128i_t v_load64_dup(const void* src) { + int64_t in; + Z_BUILTIN_MEMCPY(&in, src, sizeof(in)); + z_vec128i_t out; + for (int i = 0; i < sizeof(out); i += sizeof(in)) { + Z_BUILTIN_MEMCPY((uint8_t*)&out + i, &in, sizeof(in)); + } + return out; +} + +/* + * v_load32_dup(): load *src as an unaligned 32-bit int and duplicate it in + * every 32-bit component of the 128-bit result (32-bit int splat). + */ +static inline z_vec128i_t v_load32_dup(const void* src) { + int32_t in; + Z_BUILTIN_MEMCPY(&in, src, sizeof(in)); + z_vec128i_t out; + for (int i = 0; i < sizeof(out); i += sizeof(in)) { + Z_BUILTIN_MEMCPY((uint8_t*)&out + i, &in, sizeof(in)); + } + return out; +} + +/* + * v_load16_dup(): load *src as an unaligned 16-bit int and duplicate it in + * every 16-bit component of the 128-bit result (16-bit int splat). + */ +static inline z_vec128i_t v_load16_dup(const void* src) { + int16_t in; + Z_BUILTIN_MEMCPY(&in, src, sizeof(in)); + z_vec128i_t out; + for (int i = 0; i < sizeof(out); i += sizeof(in)) { + Z_BUILTIN_MEMCPY((uint8_t*)&out + i, &in, sizeof(in)); + } + return out; +} + +/* + * v_load8_dup(): load the 8-bit int *src and duplicate it in every 8-bit + * component of the 128-bit result (8-bit int splat). + */ +static inline z_vec128i_t v_load8_dup(const void* src) { + int8_t in = *(const uint8_t*)src; + z_vec128i_t out; + Z_BUILTIN_MEMSET(&out, in, sizeof(out)); + return out; +} + +/* + * v_store_128(): store the 128-bit vec in a memory destination (that might + * not be 16-byte aligned) void* out. + */ +static inline void v_store_128(void* out, const z_vec128i_t vec) { + Z_BUILTIN_MEMCPY(out, &vec, sizeof(vec)); +} +#endif + +/* + * Perform an overlapping copy which behaves as a memset() operation, but + * supporting periods other than one, and assume that length is non-zero and + * that it's OK to overwrite at least CHUNKCOPY_CHUNK_SIZE*3 bytes of output + * even if the length is shorter than this. + */ +static inline unsigned char FAR* chunkset_core( + unsigned char FAR* out, + unsigned period, + unsigned len) { + z_vec128i_t v; + const int bump = ((len - 1) % sizeof(v)) + 1; + + switch (period) { + case 1: + v = v_load8_dup(out - 1); + v_store_128(out, v); + out += bump; + len -= bump; + while (len > 0) { + v_store_128(out, v); + out += sizeof(v); + len -= sizeof(v); + } + return out; + case 2: + v = v_load16_dup(out - 2); + v_store_128(out, v); + out += bump; + len -= bump; + if (len > 0) { + v = v_load16_dup(out - 2); + do { + v_store_128(out, v); + out += sizeof(v); + len -= sizeof(v); + } while (len > 0); + } + return out; + case 4: + v = v_load32_dup(out - 4); + v_store_128(out, v); + out += bump; + len -= bump; + if (len > 0) { + v = v_load32_dup(out - 4); + do { + v_store_128(out, v); + out += sizeof(v); + len -= sizeof(v); + } while (len > 0); + } + return out; + case 8: + v = v_load64_dup(out - 8); + v_store_128(out, v); + out += bump; + len -= bump; + if (len > 0) { + v = v_load64_dup(out - 8); + do { + v_store_128(out, v); + out += sizeof(v); + len -= sizeof(v); + } while (len > 0); + } + return out; + } + out = chunkunroll_relaxed(out, &period, &len); + return chunkcopy_core(out, out - period, len); +} + +/* + * Perform a memcpy-like operation, but assume that length is non-zero and that + * it's OK to overwrite at least CHUNKCOPY_CHUNK_SIZE bytes of output even if + * the length is shorter than this. + * + * Unlike chunkcopy_core() above, no guarantee is made regarding the behaviour + * of overlapping buffers, regardless of the distance between the pointers. + * This is reflected in the `restrict`-qualified pointers, allowing the + * compiler to re-order loads and stores. + */ +static inline unsigned char FAR* chunkcopy_relaxed( + unsigned char FAR* Z_RESTRICT out, + const unsigned char FAR* Z_RESTRICT from, + unsigned len) { + Assert((uintptr_t)out - (uintptr_t)from >= len, + "invalid restrict in chunkcopy_relaxed"); + Assert((uintptr_t)from - (uintptr_t)out >= len, + "invalid restrict in chunkcopy_relaxed"); + return chunkcopy_core(out, from, len); +} + +/* + * Like chunkcopy_relaxed(), but avoid writing beyond of legal output. + * + * Unlike chunkcopy_core_safe() above, no guarantee is made regarding the + * behaviour of overlapping buffers, regardless of the distance between the + * pointers. This is reflected in the `restrict`-qualified pointers, allowing + * the compiler to re-order loads and stores. + * + * Accepts an additional pointer to the end of safe output. A generic safe + * copy would use (out + len), but it's normally the case that the end of the + * output buffer is beyond the end of the current copy, and this can still be + * exploited. + */ +static inline unsigned char FAR* chunkcopy_safe( + unsigned char FAR* out, + const unsigned char FAR* Z_RESTRICT from, + unsigned len, + unsigned char FAR* limit) { + Assert(out + len <= limit, "chunk copy exceeds safety limit"); + Assert((uintptr_t)out - (uintptr_t)from >= len, + "invalid restrict in chunkcopy_safe"); + Assert((uintptr_t)from - (uintptr_t)out >= len, + "invalid restrict in chunkcopy_safe"); + + return chunkcopy_core_safe(out, from, len, limit); +} + +/* + * Perform chunky copy within the same buffer, where the source and destination + * may potentially overlap. + * + * Assumes that len > 0 on entry, and that it's safe to write at least + * CHUNKCOPY_CHUNK_SIZE*3 bytes to the output. + */ +static inline unsigned char FAR* chunkcopy_lapped_relaxed( + unsigned char FAR* out, + unsigned dist, + unsigned len) { + if (dist < len && dist < CHUNKCOPY_CHUNK_SIZE) { + return chunkset_core(out, dist, len); + } + return chunkcopy_core(out, out - dist, len); +} + +/* + * Behave like chunkcopy_lapped_relaxed(), but avoid writing beyond of legal + * output. + * + * Accepts an additional pointer to the end of safe output. A generic safe + * copy would use (out + len), but it's normally the case that the end of the + * output buffer is beyond the end of the current copy, and this can still be + * exploited. + */ +static inline unsigned char FAR* chunkcopy_lapped_safe( + unsigned char FAR* out, + unsigned dist, + unsigned len, + unsigned char FAR* limit) { + Assert(out + len <= limit, "chunk copy exceeds safety limit"); + if ((limit - out) < (ptrdiff_t)(3 * CHUNKCOPY_CHUNK_SIZE)) { + /* TODO(cavalcantii): try harder to optimise this */ + while (len-- > 0) { + *out = *(out - dist); + out++; + } + return out; + } + return chunkcopy_lapped_relaxed(out, dist, len); +} + +/* TODO(cavalcanti): see crbug.com/1110083. */ +static inline unsigned char FAR* chunkcopy_safe_ugly(unsigned char FAR* out, + unsigned dist, + unsigned len, + unsigned char FAR* limit) { +#if defined(__GNUC__) && !defined(__clang__) + /* Speed is the same as using chunkcopy_safe + w/ GCC on ARM (tested gcc 6.3 and 7.5) and avoids + undefined behavior. + */ + return chunkcopy_core_safe(out, out - dist, len, limit); +#elif defined(__clang__) && defined(ARMV8_OS_ANDROID) && !defined(__aarch64__) + /* Seems to perform better on 32bit (i.e. Android). */ + return chunkcopy_core_safe(out, out - dist, len, limit); +#else + /* Seems to perform better on 64bit. */ + return chunkcopy_lapped_safe(out, dist, len, limit); +#endif +} + +/* + * The chunk-copy code above deals with writing the decoded DEFLATE data to + * the output with SIMD methods to increase decode speed. Reading the input + * to the DEFLATE decoder with a wide, SIMD method can also increase decode + * speed. This option is supported on little endian machines, and reads the + * input data in 64-bit (8 byte) chunks. + */ + +#ifdef INFLATE_CHUNK_READ_64LE +/* + * Buffer the input in a uint64_t (8 bytes) in the wide input reading case. + */ +typedef uint64_t inflate_holder_t; + +/* + * Ask the compiler to perform a wide, unaligned load of a uint64_t using a + * machine instruction appropriate for the uint64_t type. + */ +static inline inflate_holder_t read64le(const unsigned char FAR *in) { + inflate_holder_t input; + Z_BUILTIN_MEMCPY(&input, in, sizeof(input)); + return input; +} +#else +/* + * Otherwise, buffer the input bits using zlib's default input buffer type. + */ +typedef unsigned long inflate_holder_t; + +#endif /* INFLATE_CHUNK_READ_64LE */ + +#undef Z_STATIC_ASSERT +#undef Z_RESTRICT +#undef Z_BUILTIN_MEMCPY +#undef Z_DISABLE_MSAN + +#endif /* CHUNKCOPY_H */ diff --git a/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.c b/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.c new file mode 100644 index 000000000..3c93da051 --- /dev/null +++ b/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.c @@ -0,0 +1,376 @@ +/* inffast_chunk.c -- fast decoding + * Copyright (C) 1995-2026 Mark Adler + * Copyright 2023 The Chromium Authors + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +#include "zutil.h" +#include "inftrees.h" +#include "inflate.h" +#include "contrib/optimizations/inffast_chunk.h" +#include "contrib/optimizations/chunkcopy.h" + +#ifdef ASMINF +# pragma message("Assembler code may have bugs -- use at your own risk") +#else + +/* + Decode literal, length, and distance codes and write out the resulting + literal and match bytes until either not enough input or output is + available, an end-of-block is encountered, or a data error is encountered. + When large enough input and output buffers are supplied to inflate(), for + example, a 16K input buffer and a 64K output buffer, more than 95% of the + inflate() execution time is spent in this routine. + + Entry assumptions: + + state->mode == LEN + strm->avail_in >= INFLATE_FAST_MIN_INPUT (6 or 8 bytes + 7 bytes) + strm->avail_out >= INFLATE_FAST_MIN_OUTPUT (258 bytes + 2 bytes) + start >= strm->avail_out + state->bits < 8 + (state->hold >> state->bits) == 0 + strm->next_out[0..strm->avail_out] does not overlap with + strm->next_in[0..strm->avail_in] + strm->state->window is allocated with an additional + CHUNKCOPY_CHUNK_SIZE-1 bytes of padding beyond strm->state->wsize + + On return, state->mode is one of: + + LEN -- ran out of enough output space or enough available input + TYPE -- reached end of block code, inflate() to interpret next block + BAD -- error in block data + + Notes: + + INFLATE_FAST_MIN_INPUT: 6 or 8 bytes + 7 bytes + + - The maximum input bits used by a length/distance pair is 15 bits for the + length code, 5 bits for the length extra, 15 bits for the distance code, + and 13 bits for the distance extra. This totals 48 bits, or six bytes. + Therefore if strm->avail_in >= 6, then there is enough input to avoid + checking for available input while decoding. + + - The wide input data reading option reads 64 input bits at a time. Thus, + if strm->avail_in >= 8, then there is enough input to avoid checking for + available input while decoding. Reading consumes the input with: + + hold |= read64le(in) << bits; + in += 6; + bits += 48; + + reporting 6 bytes of new input because |bits| is 0..15 (2 bytes rounded + up, worst case) and 6 bytes is enough to decode as noted above. At exit, + hold &= (1U << bits) - 1 drops excess input to keep the invariant: + + (state->hold >> state->bits) == 0 + + INFLATE_FAST_MIN_OUTPUT: 258 bytes + 2 bytes for literals = 260 bytes + + - The maximum bytes that a single length/distance pair can output is 258 + bytes, which is the maximum length that can be coded. inflate_fast() + requires strm->avail_out >= 260 for each loop to avoid checking for + available output space while decoding. + */ +void ZLIB_INTERNAL inflate_fast_chunk_(z_streamp strm, unsigned start) { + struct inflate_state FAR *state; + z_const unsigned char FAR *in; /* local strm->next_in */ + z_const unsigned char FAR *last; /* have enough input while in < last */ + unsigned char FAR *out; /* local strm->next_out */ + unsigned char FAR *beg; /* inflate()'s initial strm->next_out */ + unsigned char FAR *end; /* while out < end, enough space available */ + unsigned char FAR *limit; /* safety limit for chunky copies */ +#ifdef INFLATE_STRICT + unsigned dmax; /* maximum distance from zlib header */ +#endif + unsigned wsize; /* window size or zero if not using window */ + unsigned whave; /* valid bytes in the window */ + unsigned wnext; /* window write index */ + unsigned char FAR *window; /* allocated sliding window, if wsize != 0 */ + inflate_holder_t hold; /* local strm->hold */ + unsigned bits; /* local strm->bits */ + code const FAR *lcode; /* local strm->lencode */ + code const FAR *dcode; /* local strm->distcode */ + unsigned lmask; /* mask for first level of length codes */ + unsigned dmask; /* mask for first level of distance codes */ + code const *here; /* retrieved table entry */ + unsigned op; /* code bits, operation, extra bits, or */ + /* window position, window bytes to copy */ + unsigned len; /* match length, unused bytes */ + unsigned dist; /* match distance */ + unsigned char FAR *from; /* where to copy match from */ + + /* copy state to local variables */ + state = (struct inflate_state FAR *)strm->state; + in = strm->next_in; + last = in + (strm->avail_in - (INFLATE_FAST_MIN_INPUT - 1)); + out = strm->next_out; + beg = out - (start - strm->avail_out); + end = out + (strm->avail_out - (INFLATE_FAST_MIN_OUTPUT - 1)); + limit = out + strm->avail_out; +#ifdef INFLATE_STRICT + dmax = state->dmax; +#endif + wsize = state->wsize; + whave = state->whave; + wnext = (state->wnext == 0 && whave >= wsize) ? wsize : state->wnext; + window = state->window; + hold = state->hold; + bits = state->bits; + lcode = state->lencode; + dcode = state->distcode; + lmask = (1U << state->lenbits) - 1; + dmask = (1U << state->distbits) - 1; + +#ifdef INFLATE_CHUNK_READ_64LE +#define REFILL() do { \ + Assert(bits < 64, "### Too many bits in inflate_fast."); \ + hold |= read64le(in) << bits; \ + in += 7; \ + in -= bits >> 3; \ + bits |= 56; \ + } while (0) +#endif + + /* decode literals and length/distances until end-of-block or not enough + input data or output space */ + do { +#ifdef INFLATE_CHUNK_READ_64LE + REFILL(); +#else + if (bits < 15) { + hold += (unsigned long)(*in++) << bits; + bits += 8; + hold += (unsigned long)(*in++) << bits; + bits += 8; + } +#endif + here = lcode + (hold & lmask); +#ifdef INFLATE_CHUNK_READ_64LE + if (here->op == 0) { /* literal */ + Tracevv((stderr, here->val >= 0x20 && here->val < 0x7f ? + "inflate: literal '%c'\n" : + "inflate: literal 0x%02x\n", here->val)); + *out++ = (unsigned char)(here->val); + hold >>= here->bits; + bits -= here->bits; + here = lcode + (hold & lmask); + if (here->op == 0) { /* literal */ + Tracevv((stderr, here->val >= 0x20 && here->val < 0x7f ? + "inflate: 2nd literal '%c'\n" : + "inflate: 2nd literal 0x%02x\n", here->val)); + *out++ = (unsigned char)(here->val); + hold >>= here->bits; + bits -= here->bits; + here = lcode + (hold & lmask); + } + } +#endif + dolen: + op = (unsigned)(here->bits); + hold >>= op; + bits -= op; + op = (unsigned)(here->op); + if (op == 0) { /* literal */ + Tracevv((stderr, here->val >= 0x20 && here->val < 0x7f ? + "inflate: literal '%c'\n" : + "inflate: literal 0x%02x\n", here->val)); + *out++ = (unsigned char)(here->val); + } + else if (op & 16) { /* length base */ + len = (unsigned)(here->val); + op &= 15; /* number of extra bits */ + if (op) { +#ifndef INFLATE_CHUNK_READ_64LE + if (bits < op) { + hold += (unsigned long)(*in++) << bits; + bits += 8; + } +#endif + len += (unsigned)hold & ((1U << op) - 1); + hold >>= op; + bits -= op; + } + Tracevv((stderr, "inflate: length %u\n", len)); +#ifndef INFLATE_CHUNK_READ_64LE + if (bits < 15) { + hold += (unsigned long)(*in++) << bits; + bits += 8; + hold += (unsigned long)(*in++) << bits; + bits += 8; + } +#endif + here = dcode + (hold & dmask); + dodist: + op = (unsigned)(here->bits); + hold >>= op; + bits -= op; + op = (unsigned)(here->op); + if (op & 16) { /* distance base */ + dist = (unsigned)(here->val); + op &= 15; /* number of extra bits */ + /* we have two fast-path loads: 10+10 + 15+5 + 15 = 55, + but we may need to refill here in the worst case */ + if (bits < op) { +#ifdef INFLATE_CHUNK_READ_64LE + REFILL(); +#else + hold += (unsigned long)(*in++) << bits; + bits += 8; + if (bits < op) { + hold += (unsigned long)(*in++) << bits; + bits += 8; + } +#endif + } + dist += (unsigned)hold & ((1U << op) - 1); +#ifdef INFLATE_STRICT + if (dist > dmax) { + strm->msg = (z_const char *)"invalid distance too far back"; + state->mode = BAD; + break; + } +#endif + hold >>= op; + bits -= op; + Tracevv((stderr, "inflate: distance %u\n", dist)); + op = (unsigned)(out - beg); /* max distance in output */ + if (dist > op) { /* see if copy from window */ + op = dist - op; /* distance back in window */ + if (op > whave) { + if (state->sane) { + strm->msg = + (z_const char *)"invalid distance too far back"; + state->mode = BAD; + break; + } +#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + if (len <= op - whave) { + do { + *out++ = 0; + } while (--len); + continue; + } + len -= op - whave; + do { + *out++ = 0; + } while (--op > whave); + if (op == 0) { + from = out - dist; + do { + *out++ = *from++; + } while (--len); + continue; + } +#endif + } + from = window; + if (wnext >= op) { /* contiguous in window */ + from += wnext - op; + } + else { /* wrap around window */ + op -= wnext; + from += wsize - op; + if (op < len) { /* some from end of window */ + len -= op; + out = chunkcopy_safe(out, from, op, limit); + from = window; /* more from start of window */ + op = wnext; + /* This (rare) case can create a situation where + the first chunkcopy below must be checked. + */ + } + } + if (op < len) { /* still need some from output */ + out = chunkcopy_safe(out, from, op, limit); + len -= op; + /* When dist is small the amount of data that can be + copied from the window is also small, and progress + towards the dangerous end of the output buffer is + also small. This means that for trivial memsets and + for chunkunroll_relaxed() a safety check is + unnecessary. However, these conditions may not be + entered at all, and in that case it's possible that + the main copy is near the end. + */ + out = chunkunroll_relaxed(out, &dist, &len); + out = chunkcopy_safe_ugly(out, dist, len, limit); + } else { + /* from points to window, so there is no risk of + overlapping pointers requiring memset-like behaviour + */ + out = chunkcopy_safe(out, from, len, limit); + } + } + else { + /* Whole reference is in range of current output. No + range checks are necessary because we start with room + for at least 258 bytes of output, so unroll and roundoff + operations can write beyond `out+len` so long as they + stay within 258 bytes of `out`. + */ + out = chunkcopy_lapped_relaxed(out, dist, len); + } + } + else if ((op & 64) == 0) { /* 2nd level distance code */ + here = dcode + here->val + (hold & ((1U << op) - 1)); + goto dodist; + } + else { + strm->msg = (z_const char *)"invalid distance code"; + state->mode = BAD; + break; + } + } + else if ((op & 64) == 0) { /* 2nd level length code */ + here = lcode + here->val + (hold & ((1U << op) - 1)); + goto dolen; + } + else if (op & 32) { /* end-of-block */ + Tracevv((stderr, "inflate: end of block\n")); + state->mode = TYPE; + break; + } + else { + strm->msg = (z_const char *)"invalid literal/length code"; + state->mode = BAD; + break; + } + } while (in < last && out < end); + + /* return unused bytes (on entry, bits < 8, so in won't go too far back) */ + len = bits >> 3; + in -= len; + bits -= len << 3; + hold &= (1U << bits) - 1; + + /* update state and return */ + strm->next_in = in; + strm->next_out = out; + strm->avail_in = (unsigned)(in < last ? + (INFLATE_FAST_MIN_INPUT - 1) + (last - in) : + (INFLATE_FAST_MIN_INPUT - 1) - (in - last)); + strm->avail_out = (unsigned)(out < end ? + (INFLATE_FAST_MIN_OUTPUT - 1) + (end - out) : + (INFLATE_FAST_MIN_OUTPUT - 1) - (out - end)); + state->hold = hold; + state->bits = bits; + + Assert((state->hold >> state->bits) == 0, "invalid input data state"); +} + +/* + inflate_fast() speedups that turned out slower (on a PowerPC G3 750CXe): + - Using bit fields for code structure + - Different op definition to avoid & for extra bits (do & for table bits) + - Three separate decoding do-loops for direct, window, and wnext == 0 + - Special case for distance > 1 copies to do overlapped load and store copy + - Explicit branch predictions (based on measured branch probabilities) + - Deferring match copy and interspersed it with decoding subsequent codes + - Swapping literal/length else + - Swapping window/direct else + - Larger unrolled copy loops (three is about right) + - Moving len -= 3 statement into middle of loop + */ + +#endif /* !ASMINF */ diff --git a/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.h b/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.h new file mode 100644 index 000000000..e75ee5e91 --- /dev/null +++ b/packages/runtime/vendor/zlib/contrib/optimizations/inffast_chunk.h @@ -0,0 +1,42 @@ +/* inffast_chunk.h -- header to use inffast_chunk.c + * Copyright (C) 1995-2003, 2010 Mark Adler + * Copyright (C) 2017 ARM, Inc. + * Copyright 2023 The Chromium Authors + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +/* WARNING: this file should *not* be used by applications. It is + part of the implementation of the compression library and is + subject to change. Applications should only use zlib.h. + */ + +#include "inffast.h" + +/* INFLATE_FAST_MIN_INPUT: + The minimum number of input bytes needed so that we can safely call + inflate_fast() with only one up-front bounds check. One + length/distance code pair (15 bits for the length code, 5 bits for length + extra, 15 bits for the distance code, 13 bits for distance extra) requires + reading up to 48 input bits. Additionally, in the same iteraction, we may + decode two literals from the root-table (requiring MIN_OUTPUT = 258 + 2). + + Each root-table entry is up to 10 bits, for a total of 68 input bits each + iteraction. + + The refill variant reads 8 bytes from the buffer at a time, and advances + the input pointer by up to 7 bytes, ensuring there are at least 56-bits + available in the bit-buffer. The technique was documented by Fabian Giesen + on his blog as variant 4 in the article 'Reading bits in far too many ways': + https://fgiesen.wordpress.com/2018/02/20/ + + In the worst case, we may refill twice in the same iteraction, requiring + MIN_INPUT = 8 + 7. +*/ +#ifdef INFLATE_CHUNK_READ_64LE +#undef INFLATE_FAST_MIN_INPUT +#define INFLATE_FAST_MIN_INPUT 15 +#undef INFLATE_FAST_MIN_OUTPUT +#define INFLATE_FAST_MIN_OUTPUT 260 +#endif + +void ZLIB_INTERNAL inflate_fast_chunk_(z_streamp strm, unsigned start); diff --git a/packages/runtime/vendor/zlib/contrib/optimizations/inflate.c b/packages/runtime/vendor/zlib/contrib/optimizations/inflate.c new file mode 100644 index 000000000..3268320e9 --- /dev/null +++ b/packages/runtime/vendor/zlib/contrib/optimizations/inflate.c @@ -0,0 +1,1562 @@ +/* inflate.c -- zlib decompression + * Copyright (C) 1995-2022 Mark Adler + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +/* + * Change history: + * + * 1.2.beta0 24 Nov 2002 + * - First version -- complete rewrite of inflate to simplify code, avoid + * creation of window when not needed, minimize use of window when it is + * needed, make inffast.c even faster, implement gzip decoding, and to + * improve code readability and style over the previous zlib inflate code + * + * 1.2.beta1 25 Nov 2002 + * - Use pointers for available input and output checking in inffast.c + * - Remove input and output counters in inffast.c + * - Change inffast.c entry and loop from avail_in >= 7 to >= 6 + * - Remove unnecessary second byte pull from length extra in inffast.c + * - Unroll direct copy to three copies per loop in inffast.c + * + * 1.2.beta2 4 Dec 2002 + * - Change external routine names to reduce potential conflicts + * - Correct filename to inffixed.h for fixed tables in inflate.c + * - Make hbuf[] unsigned char to match parameter type in inflate.c + * - Change strm->next_out[-state->offset] to *(strm->next_out - state->offset) + * to avoid negation problem on Alphas (64 bit) in inflate.c + * + * 1.2.beta3 22 Dec 2002 + * - Add comments on state->bits assertion in inffast.c + * - Add comments on op field in inftrees.h + * - Fix bug in reuse of allocated window after inflateReset() + * - Remove bit fields--back to byte structure for speed + * - Remove distance extra == 0 check in inflate_fast()--only helps for lengths + * - Change post-increments to pre-increments in inflate_fast(), PPC biased? + * - Add compile time option, POSTINC, to use post-increments instead (Intel?) + * - Make MATCH copy in inflate() much faster for when inflate_fast() not used + * - Use local copies of stream next and avail values, as well as local bit + * buffer and bit count in inflate()--for speed when inflate_fast() not used + * + * 1.2.beta4 1 Jan 2003 + * - Split ptr - 257 statements in inflate_table() to avoid compiler warnings + * - Move a comment on output buffer sizes from inffast.c to inflate.c + * - Add comments in inffast.c to introduce the inflate_fast() routine + * - Rearrange window copies in inflate_fast() for speed and simplification + * - Unroll last copy for window match in inflate_fast() + * - Use local copies of window variables in inflate_fast() for speed + * - Pull out common wnext == 0 case for speed in inflate_fast() + * - Make op and len in inflate_fast() unsigned for consistency + * - Add FAR to lcode and dcode declarations in inflate_fast() + * - Simplified bad distance check in inflate_fast() + * - Added inflateBackInit(), inflateBack(), and inflateBackEnd() in new + * source file infback.c to provide a call-back interface to inflate for + * programs like gzip and unzip -- uses window as output buffer to avoid + * window copying + * + * 1.2.beta5 1 Jan 2003 + * - Improved inflateBack() interface to allow the caller to provide initial + * input in strm. + * - Fixed stored blocks bug in inflateBack() + * + * 1.2.beta6 4 Jan 2003 + * - Added comments in inffast.c on effectiveness of POSTINC + * - Typecasting all around to reduce compiler warnings + * - Changed loops from while (1) or do {} while (1) to for (;;), again to + * make compilers happy + * - Changed type of window in inflateBackInit() to unsigned char * + * + * 1.2.beta7 27 Jan 2003 + * - Changed many types to unsigned or unsigned short to avoid warnings + * - Added inflateCopy() function + * + * 1.2.0 9 Mar 2003 + * - Changed inflateBack() interface to provide separate opaque descriptors + * for the in() and out() functions + * - Changed inflateBack() argument and in_func typedef to swap the length + * and buffer address return values for the input function + * - Check next_in and next_out for Z_NULL on entry to inflate() + * + * The history for versions after 1.2.0 are in ChangeLog in zlib distribution. + */ + +#include "zutil.h" +#include "inftrees.h" +#include "inflate.h" +#include "contrib/optimizations/inffast_chunk.h" +#include "contrib/optimizations/chunkcopy.h" + +#ifdef MAKEFIXED +# ifndef BUILDFIXED +# define BUILDFIXED +# endif +#endif + +local int inflateStateCheck(z_streamp strm) { + struct inflate_state FAR *state; + if (strm == Z_NULL || + strm->zalloc == (alloc_func)0 || strm->zfree == (free_func)0) + return 1; + state = (struct inflate_state FAR *)strm->state; + if (state == Z_NULL || state->strm != strm || + state->mode < HEAD || state->mode > SYNC) + return 1; + return 0; +} + +int ZEXPORT inflateResetKeep(z_streamp strm) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + strm->total_in = strm->total_out = state->total = 0; + strm->msg = Z_NULL; + if (state->wrap) /* to support ill-conceived Java test suite */ + strm->adler = state->wrap & 1; + state->mode = HEAD; + state->last = 0; + state->havedict = 0; + state->flags = -1; + state->dmax = 32768U; + state->head = Z_NULL; + state->hold = 0; + state->bits = 0; + state->lencode = state->distcode = state->next = state->codes; + state->sane = 1; + state->back = -1; + Tracev((stderr, "inflate: reset\n")); + return Z_OK; +} + +int ZEXPORT inflateReset(z_streamp strm) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + state->wsize = 0; + state->whave = 0; + state->wnext = 0; + return inflateResetKeep(strm); +} + +int ZEXPORT inflateReset2(z_streamp strm, int windowBits) { + int wrap; + struct inflate_state FAR *state; + + /* get the state */ + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + + /* extract wrap request from windowBits parameter */ + if (windowBits < 0) { + if (windowBits < -15) + return Z_STREAM_ERROR; + wrap = 0; + windowBits = -windowBits; + } + else { + wrap = (windowBits >> 4) + 5; +#ifdef GUNZIP + if (windowBits < 48) + windowBits &= 15; +#endif + } + + /* set number of window bits, free window if different */ + if (windowBits && (windowBits < 8 || windowBits > 15)) + return Z_STREAM_ERROR; + if (state->window != Z_NULL && state->wbits != (unsigned)windowBits) { + ZFREE(strm, state->window); + state->window = Z_NULL; + } + + /* update state and reset the rest of it */ + state->wrap = wrap; + state->wbits = (unsigned)windowBits; + return inflateReset(strm); +} + +int ZEXPORT inflateInit2_(z_streamp strm, int windowBits, + const char *version, int stream_size) { + int ret; + struct inflate_state FAR *state; + + if (version == Z_NULL || version[0] != ZLIB_VERSION[0] || + stream_size != (int)(sizeof(z_stream))) + return Z_VERSION_ERROR; + if (strm == Z_NULL) return Z_STREAM_ERROR; + strm->msg = Z_NULL; /* in case we return an error */ + if (strm->zalloc == (alloc_func)0) { +#ifdef Z_SOLO + return Z_STREAM_ERROR; +#else + strm->zalloc = zcalloc; + strm->opaque = (voidpf)0; +#endif + } + if (strm->zfree == (free_func)0) +#ifdef Z_SOLO + return Z_STREAM_ERROR; +#else + strm->zfree = zcfree; +#endif + state = (struct inflate_state FAR *) + ZALLOC(strm, 1, sizeof(struct inflate_state)); + if (state == Z_NULL) return Z_MEM_ERROR; + Tracev((stderr, "inflate: allocated\n")); + strm->state = (struct internal_state FAR *)state; + state->strm = strm; + state->window = Z_NULL; + state->mode = HEAD; /* to pass state test in inflateReset2() */ + state->check = 1L; /* 1L is the result of adler32() zero length data */ + ret = inflateReset2(strm, windowBits); + if (ret != Z_OK) { + ZFREE(strm, state); + strm->state = Z_NULL; + } + return ret; +} + +int ZEXPORT inflateInit_(z_streamp strm, const char *version, + int stream_size) { + return inflateInit2_(strm, DEF_WBITS, version, stream_size); +} + +int ZEXPORT inflatePrime(z_streamp strm, int bits, int value) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + if (bits == 0) + return Z_OK; + state = (struct inflate_state FAR *)strm->state; + if (bits < 0) { + state->hold = 0; + state->bits = 0; + return Z_OK; + } + if (bits > 16 || state->bits + (uInt)bits > 32) return Z_STREAM_ERROR; + value &= (1L << bits) - 1; + state->hold += (unsigned)value << state->bits; + state->bits += (uInt)bits; + return Z_OK; +} + +/* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ +local void fixedtables(struct inflate_state FAR *state) { +#ifdef BUILDFIXED + static int virgin = 1; + static code *lenfix, *distfix; + static code fixed[544]; + + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + unsigned sym, bits; + static code *next; + + /* literal/length table */ + sym = 0; + while (sym < 144) state->lens[sym++] = 8; + while (sym < 256) state->lens[sym++] = 9; + while (sym < 280) state->lens[sym++] = 7; + while (sym < 288) state->lens[sym++] = 8; + next = fixed; + lenfix = next; + bits = 9; + inflate_table(LENS, state->lens, 288, &(next), &(bits), state->work); + + /* distance table */ + sym = 0; + while (sym < 32) state->lens[sym++] = 5; + distfix = next; + bits = 5; + inflate_table(DISTS, state->lens, 32, &(next), &(bits), state->work); + + /* do this just once */ + virgin = 0; + } +#else /* !BUILDFIXED */ +# include "inffixed.h" +#endif /* BUILDFIXED */ + state->lencode = lenfix; + state->lenbits = 9; + state->distcode = distfix; + state->distbits = 5; +} + +#ifdef MAKEFIXED +#include + +/* + Write out the inffixed.h that is #include'd above. Defining MAKEFIXED also + defines BUILDFIXED, so the tables are built on the fly. makefixed() writes + those tables to stdout, which would be piped to inffixed.h. A small program + can simply call makefixed to do this: + + void makefixed(void); + + int main(void) + { + makefixed(); + return 0; + } + + Then that can be linked with zlib built with MAKEFIXED defined and run: + + a.out > inffixed.h + */ +void makefixed(void) +{ + unsigned low, size; + struct inflate_state state; + + fixedtables(&state); + puts(" /* inffixed.h -- table for decoding fixed codes"); + puts(" * Generated automatically by makefixed()."); + puts(" */"); + puts(""); + puts(" /* WARNING: this file should *not* be used by applications."); + puts(" It is part of the implementation of this library and is"); + puts(" subject to change. Applications should only use zlib.h."); + puts(" */"); + puts(""); + size = 1U << 9; + printf(" static const code lenfix[%u] = {", size); + low = 0; + for (;;) { + if ((low % 7) == 0) printf("\n "); + printf("{%u,%u,%d}", (low & 127) == 99 ? 64 : state.lencode[low].op, + state.lencode[low].bits, state.lencode[low].val); + if (++low == size) break; + putchar(','); + } + puts("\n };"); + size = 1U << 5; + printf("\n static const code distfix[%u] = {", size); + low = 0; + for (;;) { + if ((low % 6) == 0) printf("\n "); + printf("{%u,%u,%d}", state.distcode[low].op, state.distcode[low].bits, + state.distcode[low].val); + if (++low == size) break; + putchar(','); + } + puts("\n };"); +} +#endif /* MAKEFIXED */ + +/* + Update the window with the last wsize (normally 32K) bytes written before + returning. If window does not exist yet, create it. This is only called + when a window is already in use, or when output has been written during this + inflate call, but the end of the deflate stream has not been reached yet. + It is also called to create a window for dictionary data when a dictionary + is loaded. + + Providing output buffers larger than 32K to inflate() should provide a speed + advantage, since only the last 32K of output is copied to the sliding window + upon return from inflate(), and since all distances after the first 32K of + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ +local int updatewindow(z_streamp strm, const Bytef *end, unsigned copy) { + struct inflate_state FAR *state; + unsigned dist; + + state = (struct inflate_state FAR *)strm->state; + + /* if it hasn't been done already, allocate space for the window */ + if (state->window == Z_NULL) { + unsigned wsize = 1U << state->wbits; + state->window = (unsigned char FAR *) + ZALLOC(strm, wsize + CHUNKCOPY_CHUNK_SIZE, + sizeof(unsigned char)); + if (state->window == Z_NULL) return 1; +#ifdef INFLATE_CLEAR_UNUSED_UNDEFINED + /* Copies from the overflow portion of this buffer are undefined and + may cause analysis tools to raise a warning if we don't initialize + it. However, this undefined data overwrites other undefined data + and is subsequently either overwritten or left deliberately + undefined at the end of decode; so there's really no point. + */ + zmemzero(state->window + wsize, CHUNKCOPY_CHUNK_SIZE); +#endif + } + + /* if window not in use yet, initialize */ + if (state->wsize == 0) { + state->wsize = 1U << state->wbits; + state->wnext = 0; + state->whave = 0; + } + + /* copy state->wsize or less output bytes into the circular window */ + if (copy >= state->wsize) { + zmemcpy(state->window, end - state->wsize, state->wsize); + state->wnext = 0; + state->whave = state->wsize; + } + else { + dist = state->wsize - state->wnext; + if (dist > copy) dist = copy; + zmemcpy(state->window + state->wnext, end - copy, dist); + copy -= dist; + if (copy) { + zmemcpy(state->window, end - copy, copy); + state->wnext = copy; + state->whave = state->wsize; + } + else { + state->wnext += dist; + if (state->wnext == state->wsize) state->wnext = 0; + if (state->whave < state->wsize) state->whave += dist; + } + } + return 0; +} + +/* Macros for inflate(): */ + +/* check function to use adler32() for zlib or crc32() for gzip */ +#ifdef GUNZIP +# define UPDATE_CHECK(check, buf, len) \ + (state->flags ? crc32(check, buf, len) : adler32(check, buf, len)) +#else +# define UPDATE_CHECK(check, buf, len) adler32(check, buf, len) +#endif + +/* check macros for header crc */ +#ifdef GUNZIP +# define CRC2(check, word) \ + do { \ + hbuf[0] = (unsigned char)(word); \ + hbuf[1] = (unsigned char)((word) >> 8); \ + check = crc32(check, hbuf, 2); \ + } while (0) + +# define CRC4(check, word) \ + do { \ + hbuf[0] = (unsigned char)(word); \ + hbuf[1] = (unsigned char)((word) >> 8); \ + hbuf[2] = (unsigned char)((word) >> 16); \ + hbuf[3] = (unsigned char)((word) >> 24); \ + check = crc32(check, hbuf, 4); \ + } while (0) +#endif + +/* Load registers with state in inflate() for speed */ +#define LOAD() \ + do { \ + put = strm->next_out; \ + left = strm->avail_out; \ + next = strm->next_in; \ + have = strm->avail_in; \ + hold = state->hold; \ + bits = state->bits; \ + } while (0) + +/* Restore state from registers in inflate() */ +#define RESTORE() \ + do { \ + strm->next_out = put; \ + strm->avail_out = left; \ + strm->next_in = next; \ + strm->avail_in = have; \ + state->hold = hold; \ + state->bits = bits; \ + } while (0) + +/* Clear the input bit accumulator */ +#define INITBITS() \ + do { \ + hold = 0; \ + bits = 0; \ + } while (0) + +/* Get a byte of input into the bit accumulator, or return from inflate() + if there is no input available. */ +#define PULLBYTE() \ + do { \ + if (have == 0) goto inf_leave; \ + have--; \ + hold += (unsigned long)(*next++) << bits; \ + bits += 8; \ + } while (0) + +/* Assure that there are at least n bits in the bit accumulator. If there is + not enough available input to do that, then return from inflate(). */ +#define NEEDBITS(n) \ + do { \ + while (bits < (unsigned)(n)) \ + PULLBYTE(); \ + } while (0) + +/* Return the low n bits of the bit accumulator (n < 16) */ +#define BITS(n) \ + ((unsigned)hold & ((1U << (n)) - 1)) + +/* Remove n bits from the bit accumulator */ +#define DROPBITS(n) \ + do { \ + hold >>= (n); \ + bits -= (unsigned)(n); \ + } while (0) + +/* Remove zero to seven bits as needed to go to a byte boundary */ +#define BYTEBITS() \ + do { \ + hold >>= bits & 7; \ + bits -= bits & 7; \ + } while (0) + +/* + inflate() uses a state machine to process as much input data and generate as + much output data as possible before returning. The state machine is + structured roughly as follows: + + for (;;) switch (state) { + ... + case STATEn: + if (not enough input data or output space to make progress) + return; + ... make progress ... + state = STATEm; + break; + ... + } + + so when inflate() is called again, the same case is attempted again, and + if the appropriate resources are provided, the machine proceeds to the + next state. The NEEDBITS() macro is usually the way the state evaluates + whether it can proceed or should return. NEEDBITS() does the return if + the requested bits are not available. The typical use of the BITS macros + is: + + NEEDBITS(n); + ... do something with BITS(n) ... + DROPBITS(n); + + where NEEDBITS(n) either returns from inflate() if there isn't enough + input left to load n bits into the accumulator, or it continues. BITS(n) + gives the low n bits in the accumulator. When done, DROPBITS(n) drops + the low n bits off the accumulator. INITBITS() clears the accumulator + and sets the number of available bits to zero. BYTEBITS() discards just + enough bits to put the accumulator on a byte boundary. After BYTEBITS() + and a NEEDBITS(8), then BITS(8) would return the next byte in the stream. + + NEEDBITS(n) uses PULLBYTE() to get an available byte of input, or to return + if there is no input available. The decoding of variable length codes uses + PULLBYTE() directly in order to pull just enough bytes to decode the next + code, and no more. + + Some states loop until they get enough input, making sure that enough + state information is maintained to continue the loop where it left off + if NEEDBITS() returns in the loop. For example, want, need, and keep + would all have to actually be part of the saved state in case NEEDBITS() + returns: + + case STATEw: + while (want < need) { + NEEDBITS(n); + keep[want++] = BITS(n); + DROPBITS(n); + } + state = STATEx; + case STATEx: + + As shown above, if the next state is also the next case, then the break + is omitted. + + A state may also return if there is not enough output space available to + complete that state. Those states are copying stored data, writing a + literal byte, and copying a matching string. + + When returning, a "goto inf_leave" is used to update the total counters, + update the check value, and determine whether any progress has been made + during that inflate() call in order to return the proper return code. + Progress is defined as a change in either strm->avail_in or strm->avail_out. + When there is a window, goto inf_leave will update the window with the last + output written. If a goto inf_leave occurs in the middle of decompression + and there is no window currently, goto inf_leave will create one and copy + output to the window for the next call of inflate(). + + In this implementation, the flush parameter of inflate() only affects the + return code (per zlib.h). inflate() always writes as much as possible to + strm->next_out, given the space available and the provided input--the effect + documented in zlib.h of Z_SYNC_FLUSH. Furthermore, inflate() always defers + the allocation of and copying into a sliding window until necessary, which + provides the effect documented in zlib.h for Z_FINISH when the entire input + stream available. So the only thing the flush parameter actually does is: + when flush is set to Z_FINISH, inflate() cannot return Z_OK. Instead it + will return Z_BUF_ERROR if it has not reached the end of the stream. + */ + +int ZEXPORT inflate(z_streamp strm, int flush) { + struct inflate_state FAR *state; + z_const unsigned char FAR *next; /* next input */ + unsigned char FAR *put; /* next output */ + unsigned have, left; /* available input and output */ + unsigned long hold; /* bit buffer */ + unsigned bits; /* bits in bit buffer */ + unsigned in, out; /* save starting available input and output */ + unsigned copy; /* number of stored or match bytes to copy */ + unsigned char FAR *from; /* where to copy match bytes from */ + code here; /* current decoding table entry */ + code last; /* parent table entry */ + unsigned len; /* length to copy for repeats, bits to drop */ + int ret; /* return code */ +#ifdef GUNZIP + unsigned char hbuf[4]; /* buffer for gzip header crc calculation */ +#endif + static const unsigned short order[19] = /* permutation of code lengths */ + {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + + if (inflateStateCheck(strm) || strm->next_out == Z_NULL || + (strm->next_in == Z_NULL && strm->avail_in != 0)) + return Z_STREAM_ERROR; + + state = (struct inflate_state FAR *)strm->state; + if (state->mode == TYPE) state->mode = TYPEDO; /* skip check */ + LOAD(); + in = have; + out = left; + ret = Z_OK; + for (;;) + switch (state->mode) { + case HEAD: + if (state->wrap == 0) { + state->mode = TYPEDO; + break; + } + NEEDBITS(16); +#ifdef GUNZIP + if ((state->wrap & 2) && hold == 0x8b1f) { /* gzip header */ + if (state->wbits == 0) + state->wbits = 15; + state->check = crc32(0L, Z_NULL, 0); + CRC2(state->check, hold); + INITBITS(); + state->mode = FLAGS; + break; + } + if (state->head != Z_NULL) + state->head->done = -1; + if (!(state->wrap & 1) || /* check if zlib header allowed */ +#else + if ( +#endif + ((BITS(8) << 8) + (hold >> 8)) % 31) { + strm->msg = (char *)"incorrect header check"; + state->mode = BAD; + break; + } + if (BITS(4) != Z_DEFLATED) { + strm->msg = (char *)"unknown compression method"; + state->mode = BAD; + break; + } + DROPBITS(4); + len = BITS(4) + 8; + if (state->wbits == 0) + state->wbits = len; + if (len > 15 || len > state->wbits) { + strm->msg = (char *)"invalid window size"; + state->mode = BAD; + break; + } + state->dmax = 1U << len; + state->flags = 0; /* indicate zlib header */ + Tracev((stderr, "inflate: zlib header ok\n")); + strm->adler = state->check = adler32(0L, Z_NULL, 0); + state->mode = hold & 0x200 ? DICTID : TYPE; + INITBITS(); + break; +#ifdef GUNZIP + case FLAGS: + NEEDBITS(16); + state->flags = (int)(hold); + if ((state->flags & 0xff) != Z_DEFLATED) { + strm->msg = (char *)"unknown compression method"; + state->mode = BAD; + break; + } + if (state->flags & 0xe000) { + strm->msg = (char *)"unknown header flags set"; + state->mode = BAD; + break; + } + if (state->head != Z_NULL) + state->head->text = (int)((hold >> 8) & 1); + if ((state->flags & 0x0200) && (state->wrap & 4)) + CRC2(state->check, hold); + INITBITS(); + state->mode = TIME; + /* fallthrough */ + case TIME: + NEEDBITS(32); + if (state->head != Z_NULL) + state->head->time = hold; + if ((state->flags & 0x0200) && (state->wrap & 4)) + CRC4(state->check, hold); + INITBITS(); + state->mode = OS; + /* fallthrough */ + case OS: + NEEDBITS(16); + if (state->head != Z_NULL) { + state->head->xflags = (int)(hold & 0xff); + state->head->os = (int)(hold >> 8); + } + if ((state->flags & 0x0200) && (state->wrap & 4)) + CRC2(state->check, hold); + INITBITS(); + state->mode = EXLEN; + /* fallthrough */ + case EXLEN: + if (state->flags & 0x0400) { + NEEDBITS(16); + state->length = (unsigned)(hold); + if (state->head != Z_NULL) + state->head->extra_len = (unsigned)hold; + if ((state->flags & 0x0200) && (state->wrap & 4)) + CRC2(state->check, hold); + INITBITS(); + } + else if (state->head != Z_NULL) + state->head->extra = Z_NULL; + state->mode = EXTRA; + /* fallthrough */ + case EXTRA: + if (state->flags & 0x0400) { + copy = state->length; + if (copy > have) copy = have; + if (copy) { + if (state->head != Z_NULL && + state->head->extra != Z_NULL && + (len = state->head->extra_len - state->length) < + state->head->extra_max) { + zmemcpy(state->head->extra + len, next, + len + copy > state->head->extra_max ? + state->head->extra_max - len : copy); + } + if ((state->flags & 0x0200) && (state->wrap & 4)) + state->check = crc32(state->check, next, copy); + have -= copy; + next += copy; + state->length -= copy; + } + if (state->length) goto inf_leave; + } + state->length = 0; + state->mode = NAME; + /* fallthrough */ + case NAME: + if (state->flags & 0x0800) { + if (have == 0) goto inf_leave; + copy = 0; + do { + len = (unsigned)(next[copy++]); + if (state->head != Z_NULL && + state->head->name != Z_NULL && + state->length < state->head->name_max) + state->head->name[state->length++] = (Bytef)len; + } while (len && copy < have); + if ((state->flags & 0x0200) && (state->wrap & 4)) + state->check = crc32(state->check, next, copy); + have -= copy; + next += copy; + if (len) goto inf_leave; + } + else if (state->head != Z_NULL) + state->head->name = Z_NULL; + state->length = 0; + state->mode = COMMENT; + /* fallthrough */ + case COMMENT: + if (state->flags & 0x1000) { + if (have == 0) goto inf_leave; + copy = 0; + do { + len = (unsigned)(next[copy++]); + if (state->head != Z_NULL && + state->head->comment != Z_NULL && + state->length < state->head->comm_max) + state->head->comment[state->length++] = (Bytef)len; + } while (len && copy < have); + if ((state->flags & 0x0200) && (state->wrap & 4)) + state->check = crc32(state->check, next, copy); + have -= copy; + next += copy; + if (len) goto inf_leave; + } + else if (state->head != Z_NULL) + state->head->comment = Z_NULL; + state->mode = HCRC; + /* fallthrough */ + case HCRC: + if (state->flags & 0x0200) { + NEEDBITS(16); + if ((state->wrap & 4) && hold != (state->check & 0xffff)) { + strm->msg = (char *)"header crc mismatch"; + state->mode = BAD; + break; + } + INITBITS(); + } + if (state->head != Z_NULL) { + state->head->hcrc = (int)((state->flags >> 9) & 1); + state->head->done = 1; + } + strm->adler = state->check = crc32(0L, Z_NULL, 0); + state->mode = TYPE; + break; +#endif + case DICTID: + NEEDBITS(32); + strm->adler = state->check = ZSWAP32(hold); + INITBITS(); + state->mode = DICT; + /* fallthrough */ + case DICT: + if (state->havedict == 0) { + RESTORE(); + return Z_NEED_DICT; + } + strm->adler = state->check = adler32(0L, Z_NULL, 0); + state->mode = TYPE; + /* fallthrough */ + case TYPE: + if (flush == Z_BLOCK || flush == Z_TREES) goto inf_leave; + /* fallthrough */ + case TYPEDO: + if (state->last) { + BYTEBITS(); + state->mode = CHECK; + break; + } + NEEDBITS(3); + state->last = BITS(1); + DROPBITS(1); + switch (BITS(2)) { + case 0: /* stored block */ + Tracev((stderr, "inflate: stored block%s\n", + state->last ? " (last)" : "")); + state->mode = STORED; + break; + case 1: /* fixed block */ + fixedtables(state); + Tracev((stderr, "inflate: fixed codes block%s\n", + state->last ? " (last)" : "")); + state->mode = LEN_; /* decode codes */ + if (flush == Z_TREES) { + DROPBITS(2); + goto inf_leave; + } + break; + case 2: /* dynamic block */ + Tracev((stderr, "inflate: dynamic codes block%s\n", + state->last ? " (last)" : "")); + state->mode = TABLE; + break; + case 3: + strm->msg = (char *)"invalid block type"; + state->mode = BAD; + } + DROPBITS(2); + break; + case STORED: + BYTEBITS(); /* go to byte boundary */ + NEEDBITS(32); + if ((hold & 0xffff) != ((hold >> 16) ^ 0xffff)) { + strm->msg = (char *)"invalid stored block lengths"; + state->mode = BAD; + break; + } + state->length = (unsigned)hold & 0xffff; + Tracev((stderr, "inflate: stored length %u\n", + state->length)); + INITBITS(); + state->mode = COPY_; + if (flush == Z_TREES) goto inf_leave; + /* fallthrough */ + case COPY_: + state->mode = COPY; + /* fallthrough */ + case COPY: + copy = state->length; + if (copy) { + if (copy > have) copy = have; + if (copy > left) copy = left; + if (copy == 0) goto inf_leave; + zmemcpy(put, next, copy); + have -= copy; + next += copy; + left -= copy; + put += copy; + state->length -= copy; + break; + } + Tracev((stderr, "inflate: stored end\n")); + state->mode = TYPE; + break; + case TABLE: + NEEDBITS(14); + state->nlen = BITS(5) + 257; + DROPBITS(5); + state->ndist = BITS(5) + 1; + DROPBITS(5); + state->ncode = BITS(4) + 4; + DROPBITS(4); +#ifndef PKZIP_BUG_WORKAROUND + if (state->nlen > 286 || state->ndist > 30) { + strm->msg = (char *)"too many length or distance symbols"; + state->mode = BAD; + break; + } +#endif + Tracev((stderr, "inflate: table sizes ok\n")); + state->have = 0; + state->mode = LENLENS; + /* fallthrough */ + case LENLENS: + while (state->have < state->ncode) { + NEEDBITS(3); + state->lens[order[state->have++]] = (unsigned short)BITS(3); + DROPBITS(3); + } + while (state->have < 19) + state->lens[order[state->have++]] = 0; + state->next = state->codes; + state->lencode = state->distcode = (const code FAR *)(state->next); + state->lenbits = 7; + ret = inflate_table(CODES, state->lens, 19, &(state->next), + &(state->lenbits), state->work); + if (ret) { + strm->msg = (char *)"invalid code lengths set"; + state->mode = BAD; + break; + } + Tracev((stderr, "inflate: code lengths ok\n")); + state->have = 0; + state->mode = CODELENS; + /* fallthrough */ + case CODELENS: + while (state->have < state->nlen + state->ndist) { + for (;;) { + here = state->lencode[BITS(state->lenbits)]; + if ((unsigned)(here.bits) <= bits) break; + PULLBYTE(); + } + if (here.val < 16) { + DROPBITS(here.bits); + state->lens[state->have++] = here.val; + } + else { + if (here.val == 16) { + NEEDBITS(here.bits + 2); + DROPBITS(here.bits); + if (state->have == 0) { + strm->msg = (char *)"invalid bit length repeat"; + state->mode = BAD; + break; + } + len = state->lens[state->have - 1]; + copy = 3 + BITS(2); + DROPBITS(2); + } + else if (here.val == 17) { + NEEDBITS(here.bits + 3); + DROPBITS(here.bits); + len = 0; + copy = 3 + BITS(3); + DROPBITS(3); + } + else { + NEEDBITS(here.bits + 7); + DROPBITS(here.bits); + len = 0; + copy = 11 + BITS(7); + DROPBITS(7); + } + if (state->have + copy > state->nlen + state->ndist) { + strm->msg = (char *)"invalid bit length repeat"; + state->mode = BAD; + break; + } + while (copy--) + state->lens[state->have++] = (unsigned short)len; + } + } + + /* handle error breaks in while */ + if (state->mode == BAD) break; + + /* check for end-of-block code (better have one) */ + if (state->lens[256] == 0) { + strm->msg = (char *)"invalid code -- missing end-of-block"; + state->mode = BAD; + break; + } + + /* build code tables -- note: do not change the lenbits or distbits + values here (10 and 9) without reading the comments in inftrees.h + concerning the ENOUGH constants, which depend on those values */ + state->next = state->codes; + state->lencode = (const code FAR *)(state->next); + state->lenbits = 10; + ret = inflate_table(LENS, state->lens, state->nlen, &(state->next), + &(state->lenbits), state->work); + if (ret) { + strm->msg = (char *)"invalid literal/lengths set"; + state->mode = BAD; + break; + } + state->distcode = (const code FAR *)(state->next); + state->distbits = 9; + ret = inflate_table(DISTS, state->lens + state->nlen, state->ndist, + &(state->next), &(state->distbits), state->work); + if (ret) { + strm->msg = (char *)"invalid distances set"; + state->mode = BAD; + break; + } + Tracev((stderr, "inflate: codes ok\n")); + state->mode = LEN_; + if (flush == Z_TREES) goto inf_leave; + /* fallthrough */ + case LEN_: + state->mode = LEN; + /* fallthrough */ + case LEN: + if (have >= INFLATE_FAST_MIN_INPUT && + left >= INFLATE_FAST_MIN_OUTPUT) { + RESTORE(); + inflate_fast_chunk_(strm, out); + LOAD(); + if (state->mode == TYPE) + state->back = -1; + break; + } + state->back = 0; + for (;;) { + here = state->lencode[BITS(state->lenbits)]; + if ((unsigned)(here.bits) <= bits) break; + PULLBYTE(); + } + if (here.op && (here.op & 0xf0) == 0) { + last = here; + for (;;) { + here = state->lencode[last.val + + (BITS(last.bits + last.op) >> last.bits)]; + if ((unsigned)(last.bits + here.bits) <= bits) break; + PULLBYTE(); + } + DROPBITS(last.bits); + state->back += last.bits; + } + DROPBITS(here.bits); + state->back += here.bits; + state->length = (unsigned)here.val; + if ((int)(here.op) == 0) { + Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + "inflate: literal '%c'\n" : + "inflate: literal 0x%02x\n", here.val)); + state->mode = LIT; + break; + } + if (here.op & 32) { + Tracevv((stderr, "inflate: end of block\n")); + state->back = -1; + state->mode = TYPE; + break; + } + if (here.op & 64) { + strm->msg = (char *)"invalid literal/length code"; + state->mode = BAD; + break; + } + state->extra = (unsigned)(here.op) & 15; + state->mode = LENEXT; + /* fallthrough */ + case LENEXT: + if (state->extra) { + NEEDBITS(state->extra); + state->length += BITS(state->extra); + DROPBITS(state->extra); + state->back += state->extra; + } + Tracevv((stderr, "inflate: length %u\n", state->length)); + state->was = state->length; + state->mode = DIST; + /* fallthrough */ + case DIST: + for (;;) { + here = state->distcode[BITS(state->distbits)]; + if ((unsigned)(here.bits) <= bits) break; + PULLBYTE(); + } + if ((here.op & 0xf0) == 0) { + last = here; + for (;;) { + here = state->distcode[last.val + + (BITS(last.bits + last.op) >> last.bits)]; + if ((unsigned)(last.bits + here.bits) <= bits) break; + PULLBYTE(); + } + DROPBITS(last.bits); + state->back += last.bits; + } + DROPBITS(here.bits); + state->back += here.bits; + if (here.op & 64) { + strm->msg = (char *)"invalid distance code"; + state->mode = BAD; + break; + } + state->offset = (unsigned)here.val; + state->extra = (unsigned)(here.op) & 15; + state->mode = DISTEXT; + /* fallthrough */ + case DISTEXT: + if (state->extra) { + NEEDBITS(state->extra); + state->offset += BITS(state->extra); + DROPBITS(state->extra); + state->back += state->extra; + } +#ifdef INFLATE_STRICT + if (state->offset > state->dmax) { + strm->msg = (char *)"invalid distance too far back"; + state->mode = BAD; + break; + } +#endif + Tracevv((stderr, "inflate: distance %u\n", state->offset)); + state->mode = MATCH; + /* fallthrough */ + case MATCH: + if (left == 0) goto inf_leave; + copy = out - left; + if (state->offset > copy) { /* copy from window */ + copy = state->offset - copy; + if (copy > state->whave) { + if (state->sane) { + strm->msg = (char *)"invalid distance too far back"; + state->mode = BAD; + break; + } +#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + Trace((stderr, "inflate.c too far\n")); + copy -= state->whave; + if (copy > state->length) copy = state->length; + if (copy > left) copy = left; + left -= copy; + state->length -= copy; + do { + *put++ = 0; + } while (--copy); + if (state->length == 0) state->mode = LEN; + break; +#endif + } + if (copy > state->wnext) { + copy -= state->wnext; + from = state->window + (state->wsize - copy); + } + else + from = state->window + (state->wnext - copy); + if (copy > state->length) copy = state->length; + if (copy > left) copy = left; + put = chunkcopy_safe(put, from, copy, put + left); + } + else { /* copy from output */ + copy = state->length; + if (copy > left) copy = left; + put = chunkcopy_lapped_safe(put, state->offset, copy, put + left); + } + left -= copy; + state->length -= copy; + if (state->length == 0) state->mode = LEN; + break; + case LIT: + if (left == 0) goto inf_leave; + *put++ = (unsigned char)(state->length); + left--; + state->mode = LEN; + break; + case CHECK: + if (state->wrap) { + NEEDBITS(32); + out -= left; + strm->total_out += out; + state->total += out; + if ((state->wrap & 4) && out) + strm->adler = state->check = + UPDATE_CHECK(state->check, put - out, out); + out = left; + if ((state->wrap & 4) && ( +#ifdef GUNZIP + state->flags ? hold : +#endif + ZSWAP32(hold)) != state->check) { + strm->msg = (char *)"incorrect data check"; + state->mode = BAD; + break; + } + INITBITS(); + Tracev((stderr, "inflate: check matches trailer\n")); + } +#ifdef GUNZIP + state->mode = LENGTH; + /* fallthrough */ + case LENGTH: + if (state->wrap && state->flags) { + NEEDBITS(32); + if ((state->wrap & 4) && hold != (state->total & 0xffffffff)) { + strm->msg = (char *)"incorrect length check"; + state->mode = BAD; + break; + } + INITBITS(); + Tracev((stderr, "inflate: length matches trailer\n")); + } +#endif + state->mode = DONE; + /* fallthrough */ + case DONE: + ret = Z_STREAM_END; + goto inf_leave; + case BAD: + ret = Z_DATA_ERROR; + goto inf_leave; + case MEM: + return Z_MEM_ERROR; + case SYNC: + /* fallthrough */ + default: + return Z_STREAM_ERROR; + } + + /* + Return from inflate(), updating the total counts and the check value. + If there was no progress during the inflate() call, return a buffer + error. Call updatewindow() to create and/or update the window state. + Note: a memory error from inflate() is non-recoverable. + */ + inf_leave: +#if defined(ZLIB_DEBUG) + /* XXX(cavalcantii): I put this in place back in 2017 to help debug faulty + * client code relying on undefined behavior when chunk_copy first landed. + * + * It is save to say after all these years that Chromium code is well + * behaved and works fine with the optimization, therefore we can enable + * this only for DEBUG builds. + * + * We write a defined value in the unused space to help mark + * where the stream has ended. We don't use zeros as that can + * mislead clients relying on undefined behavior (i.e. assuming + * that the data is over when the buffer has a zero/null value). + * + * The basic idea is that if client code is not relying on the zlib context + * to inform the amount of decompressed data, but instead reads the output + * buffer until a zero/null is found, it will fail faster and harder + * when the remaining of the buffer is marked with a symbol (e.g. 0x55). + */ + if (left >= CHUNKCOPY_CHUNK_SIZE) + memset(put, 0x55, CHUNKCOPY_CHUNK_SIZE); + else + memset(put, 0x55, left); +#endif + RESTORE(); + if (state->wsize || (out != strm->avail_out && state->mode < BAD && + (state->mode < CHECK || flush != Z_FINISH))) + if (updatewindow(strm, strm->next_out, out - strm->avail_out)) { + state->mode = MEM; + return Z_MEM_ERROR; + } + in -= strm->avail_in; + out -= strm->avail_out; + strm->total_in += in; + strm->total_out += out; + state->total += out; + if ((state->wrap & 4) && out) + strm->adler = state->check = + UPDATE_CHECK(state->check, strm->next_out - out, out); + strm->data_type = (int)state->bits + (state->last ? 64 : 0) + + (state->mode == TYPE ? 128 : 0) + + (state->mode == LEN_ || state->mode == COPY_ ? 256 : 0); + if (((in == 0 && out == 0) || flush == Z_FINISH) && ret == Z_OK) + ret = Z_BUF_ERROR; + return ret; +} + +int ZEXPORT inflateEnd(z_streamp strm) { + struct inflate_state FAR *state; + if (inflateStateCheck(strm)) + return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + if (state->window != Z_NULL) ZFREE(strm, state->window); + ZFREE(strm, strm->state); + strm->state = Z_NULL; + Tracev((stderr, "inflate: end\n")); + return Z_OK; +} + +int ZEXPORT inflateGetDictionary(z_streamp strm, Bytef *dictionary, + uInt *dictLength) { + struct inflate_state FAR *state; + + /* check state */ + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + + /* copy dictionary */ + if (state->whave && dictionary != Z_NULL) { + zmemcpy(dictionary, state->window + state->wnext, + state->whave - state->wnext); + zmemcpy(dictionary + state->whave - state->wnext, + state->window, state->wnext); + } + if (dictLength != Z_NULL) + *dictLength = state->whave; + return Z_OK; +} + +int ZEXPORT inflateSetDictionary(z_streamp strm, const Bytef *dictionary, + uInt dictLength) { + struct inflate_state FAR *state; + unsigned long dictid; + int ret; + + /* check state */ + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + if (state->wrap != 0 && state->mode != DICT) + return Z_STREAM_ERROR; + + /* check for correct dictionary identifier */ + if (state->mode == DICT) { + dictid = adler32(0L, Z_NULL, 0); + dictid = adler32(dictid, dictionary, dictLength); + if (dictid != state->check) + return Z_DATA_ERROR; + } + + /* copy dictionary to window using updatewindow(), which will amend the + existing dictionary if appropriate */ + ret = updatewindow(strm, dictionary + dictLength, dictLength); + if (ret) { + state->mode = MEM; + return Z_MEM_ERROR; + } + state->havedict = 1; + Tracev((stderr, "inflate: dictionary set\n")); + return Z_OK; +} + +int ZEXPORT inflateGetHeader(z_streamp strm, gz_headerp head) { + struct inflate_state FAR *state; + + /* check state */ + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + if ((state->wrap & 2) == 0) return Z_STREAM_ERROR; + + /* save header structure */ + state->head = head; + head->done = 0; + return Z_OK; +} + +/* + Search buf[0..len-1] for the pattern: 0, 0, 0xff, 0xff. Return when found + or when out of input. When called, *have is the number of pattern bytes + found in order so far, in 0..3. On return *have is updated to the new + state. If on return *have equals four, then the pattern was found and the + return value is how many bytes were read including the last byte of the + pattern. If *have is less than four, then the pattern has not been found + yet and the return value is len. In the latter case, syncsearch() can be + called again with more data and the *have state. *have is initialized to + zero for the first call. + */ +local unsigned syncsearch(unsigned FAR *have, const unsigned char FAR *buf, + unsigned len) { + unsigned got; + unsigned next; + + got = *have; + next = 0; + while (next < len && got < 4) { + if ((int)(buf[next]) == (got < 2 ? 0 : 0xff)) + got++; + else if (buf[next]) + got = 0; + else + got = 4 - got; + next++; + } + *have = got; + return next; +} + +int ZEXPORT inflateSync(z_streamp strm) { + unsigned len; /* number of bytes to look at or looked at */ + int flags; /* temporary to save header status */ + unsigned long in, out; /* temporary to save total_in and total_out */ + unsigned char buf[4]; /* to restore bit buffer to byte string */ + struct inflate_state FAR *state; + + /* check parameters */ + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + if (strm->avail_in == 0 && state->bits < 8) return Z_BUF_ERROR; + + /* if first time, start search in bit buffer */ + if (state->mode != SYNC) { + state->mode = SYNC; + state->hold >>= state->bits & 7; + state->bits -= state->bits & 7; + len = 0; + while (state->bits >= 8) { + buf[len++] = (unsigned char)(state->hold); + state->hold >>= 8; + state->bits -= 8; + } + state->have = 0; + syncsearch(&(state->have), buf, len); + } + + /* search available input */ + len = syncsearch(&(state->have), strm->next_in, strm->avail_in); + strm->avail_in -= len; + strm->next_in += len; + strm->total_in += len; + + /* return no joy or set up to restart inflate() on a new block */ + if (state->have != 4) return Z_DATA_ERROR; + if (state->flags == -1) + state->wrap = 0; /* if no header yet, treat as raw */ + else + state->wrap &= ~4; /* no point in computing a check value now */ + flags = state->flags; + in = strm->total_in; out = strm->total_out; + inflateReset(strm); + strm->total_in = in; strm->total_out = out; + state->flags = flags; + state->mode = TYPE; + return Z_OK; +} + +/* + Returns true if inflate is currently at the end of a block generated by + Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP + implementation to provide an additional safety check. PPP uses + Z_SYNC_FLUSH but removes the length bytes of the resulting empty stored + block. When decompressing, PPP checks that at the end of input packet, + inflate is waiting for these length bytes. + */ +int ZEXPORT inflateSyncPoint(z_streamp strm) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + return state->mode == STORED && state->bits == 0; +} + +int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) { + struct inflate_state FAR *state; + struct inflate_state FAR *copy; + unsigned char FAR *window; + unsigned wsize; + + /* check input */ + if (inflateStateCheck(source) || dest == Z_NULL) + return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)source->state; + + /* allocate space */ + copy = (struct inflate_state FAR *) + ZALLOC(source, 1, sizeof(struct inflate_state)); + if (copy == Z_NULL) return Z_MEM_ERROR; + window = Z_NULL; + if (state->window != Z_NULL) { + window = (unsigned char FAR *)ZALLOC( + source, (1U << state->wbits) + CHUNKCOPY_CHUNK_SIZE, + sizeof(unsigned char)); + if (window == Z_NULL) { + ZFREE(source, copy); + return Z_MEM_ERROR; + } + } + + /* copy state */ + zmemcpy((voidpf)dest, (voidpf)source, sizeof(z_stream)); + zmemcpy((voidpf)copy, (voidpf)state, sizeof(struct inflate_state)); + copy->strm = dest; + if (state->lencode >= state->codes && + state->lencode <= state->codes + ENOUGH - 1) { + copy->lencode = copy->codes + (state->lencode - state->codes); + copy->distcode = copy->codes + (state->distcode - state->codes); + } + copy->next = copy->codes + (state->next - state->codes); + if (window != Z_NULL) { + wsize = 1U << state->wbits; + zmemcpy(window, state->window, wsize); + } + copy->window = window; + dest->state = (struct internal_state FAR *)copy; + return Z_OK; +} + +int ZEXPORT inflateUndermine(z_streamp strm, int subvert) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; +#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + state->sane = !subvert; + return Z_OK; +#else + (void)subvert; + state->sane = 1; + return Z_DATA_ERROR; +#endif +} + +int ZEXPORT inflateValidate(z_streamp strm, int check) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) return Z_STREAM_ERROR; + state = (struct inflate_state FAR *)strm->state; + if (check && state->wrap) + state->wrap |= 4; + else + state->wrap &= ~4; + return Z_OK; +} + +long ZEXPORT inflateMark(z_streamp strm) { + struct inflate_state FAR *state; + + if (inflateStateCheck(strm)) + return -(1L << 16); + state = (struct inflate_state FAR *)strm->state; + return (long)(((unsigned long)((long)state->back)) << 16) + + (state->mode == COPY ? state->length : + (state->mode == MATCH ? state->was - state->length : 0)); +} + +unsigned long ZEXPORT inflateCodesUsed(z_streamp strm) { + struct inflate_state FAR *state; + if (inflateStateCheck(strm)) return (unsigned long)-1; + state = (struct inflate_state FAR *)strm->state; + return (unsigned long)(state->next - state->codes); +} diff --git a/packages/runtime/vendor/zlib/contrib/optimizations/insert_string.h b/packages/runtime/vendor/zlib/contrib/optimizations/insert_string.h new file mode 100644 index 000000000..260b82680 --- /dev/null +++ b/packages/runtime/vendor/zlib/contrib/optimizations/insert_string.h @@ -0,0 +1,80 @@ +/* insert_string.h + * + * Copyright 2019 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#ifndef INSERT_STRING_H +#define INSERT_STRING_H + +#ifndef INLINE +#if defined(_MSC_VER) && !defined(__clang__) +#define INLINE __inline +#else +#define INLINE inline +#endif +#endif + +#include + +/** + * Some applications need to match zlib DEFLATE output exactly [3]. Use the + * canonical zlib Rabin-Karp rolling hash [1,2] in that case. + * + * [1] For a description of the Rabin and Karp algorithm, see "Algorithms" + * book by R. Sedgewick, Addison-Wesley, p252. + * [2] https://www.euccas.me/zlib/#zlib_rabin_karp and also "rolling hash" + * https://en.wikipedia.org/wiki/Rolling_hash + * [3] crbug.com/1316541 AOSP incremental client APK package OTA upgrades. + */ +#ifdef CHROMIUM_ZLIB_NO_CASTAGNOLI +#define USE_ZLIB_RABIN_KARP_ROLLING_HASH +#endif + +/* =========================================================================== + * Update a hash value with the given input byte (Rabin-Karp rolling hash). + * IN assertion: all calls to UPDATE_HASH are made with consecutive input + * characters, so that a running hash key can be computed from the previous + * key instead of complete recalculation each time. + */ +#define UPDATE_HASH(s, h, c) (h = (((h) << s->hash_shift) ^ (c)) & s->hash_mask) + +/* =========================================================================== + * Insert string str in the dictionary and set match_head to the previous head + * of the hash chain (the most recent string with same hash key). Return + * the previous length of the hash chain. + * If this file is compiled with -DFASTEST, the compression level is forced + * to 1, and no hash chains are maintained. + * IN assertion: all calls to INSERT_STRING are made with consecutive input + * characters and the first MIN_MATCH bytes of str are valid (except for + * the last MIN_MATCH-1 bytes of the input file). + */ +local INLINE Pos insert_string(deflate_state* const s, const Pos str) { + Pos ret; +/* insert_string dictionary insertion: ANZAC++ hasher + * significantly improves data compression speed. + * + * Note: the generated compressed output is a valid DEFLATE stream, but will + * differ from canonical zlib output. + */ +#if defined(USE_ZLIB_RABIN_KARP_ROLLING_HASH) + UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH - 1)]); +#else + uint32_t value; + // Validated for little endian archs (i.e. x86, Arm). YMMV for big endian. + zmemcpy(&value, &s->window[str], sizeof(value)); + s->ins_h = ((value * 66521 + 66521) >> 16) & s->hash_mask; +#endif + +#ifdef FASTEST + ret = s->head[s->ins_h]; +#else + ret = s->prev[str & s->w_mask] = s->head[s->ins_h]; +#endif + s->head[s->ins_h] = str; + + return ret; +} + +#endif /* INSERT_STRING_H */ diff --git a/packages/runtime/vendor/zlib/cpu_features.c b/packages/runtime/vendor/zlib/cpu_features.c new file mode 100644 index 000000000..6613a65e3 --- /dev/null +++ b/packages/runtime/vendor/zlib/cpu_features.c @@ -0,0 +1,218 @@ +/* cpu_features.c -- Processor features detection. + * + * Copyright 2018 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#include "cpu_features.h" +#include "zutil.h" + +#include +#if defined(_MSC_VER) +#include +#elif defined(ADLER32_SIMD_SSSE3) +#include +#endif + +/* TODO(cavalcantii): remove checks for x86_flags on deflate. + */ +#if defined(ARMV8_OS_MACOS) +/* Crypto extensions (crc32/pmull) are a baseline feature in ARMv8.1-A, and + * OSX running on arm64 is new enough that these can be assumed without + * runtime detection. + */ +int ZLIB_INTERNAL arm_cpu_enable_crc32 = 1; +int ZLIB_INTERNAL arm_cpu_enable_pmull = 1; +#else +int ZLIB_INTERNAL arm_cpu_enable_crc32 = 0; +int ZLIB_INTERNAL arm_cpu_enable_pmull = 0; +#endif +int ZLIB_INTERNAL x86_cpu_enable_sse2 = 0; +int ZLIB_INTERNAL x86_cpu_enable_ssse3 = 0; +int ZLIB_INTERNAL x86_cpu_enable_simd = 0; +int ZLIB_INTERNAL x86_cpu_enable_avx512 = 0; + +int ZLIB_INTERNAL riscv_cpu_enable_rvv = 0; +int ZLIB_INTERNAL riscv_cpu_enable_vclmul = 0; + +#ifndef CPU_NO_SIMD + +#if defined(ARMV8_OS_ANDROID) || defined(ARMV8_OS_LINUX) || \ + defined(ARMV8_OS_FUCHSIA) || defined(ARMV8_OS_IOS) +#include +#endif + +#if defined(ARMV8_OS_ANDROID) +#include +#elif defined(ARMV8_OS_LINUX) +#include +#include +#elif defined(ARMV8_OS_FUCHSIA) +#include +#include +#include +#elif defined(ARMV8_OS_WINDOWS) || defined(X86_WINDOWS) +#include +#elif defined(ARMV8_OS_IOS) +#include +#elif !defined(_MSC_VER) +#include +#else +#error cpu_features.c CPU feature detection in not defined for your platform +#endif + +#if !defined(CPU_NO_SIMD) && !defined(ARMV8_OS_MACOS) +static void _cpu_check_features(void); +#endif + +#if defined(ARMV8_OS_ANDROID) || defined(ARMV8_OS_LINUX) || \ + defined(ARMV8_OS_MACOS) || defined(ARMV8_OS_FUCHSIA) || \ + defined(X86_NOT_WINDOWS) || defined(ARMV8_OS_IOS) || \ + defined(RISCV_RVV) +#if !defined(ARMV8_OS_MACOS) +// _cpu_check_features() doesn't need to do anything on mac/arm since all +// features are known at build time, so don't call it. +// Do provide cpu_check_features() (with a no-op implementation) so that we +// don't have to make all callers of it check for mac/arm. +static pthread_once_t cpu_check_inited_once = PTHREAD_ONCE_INIT; +#endif +void ZLIB_INTERNAL cpu_check_features(void) +{ +#if !defined(ARMV8_OS_MACOS) + pthread_once(&cpu_check_inited_once, _cpu_check_features); +#endif +} +#elif defined(ARMV8_OS_WINDOWS) || defined(X86_WINDOWS) +static INIT_ONCE cpu_check_inited_once = INIT_ONCE_STATIC_INIT; +static BOOL CALLBACK _cpu_check_features_forwarder(PINIT_ONCE once, PVOID param, PVOID* context) +{ + _cpu_check_features(); + return TRUE; +} +void ZLIB_INTERNAL cpu_check_features(void) +{ + InitOnceExecuteOnce(&cpu_check_inited_once, _cpu_check_features_forwarder, + NULL, NULL); +} +#endif + +#if (defined(__ARM_NEON__) || defined(__ARM_NEON)) +#if !defined(ARMV8_OS_MACOS) +/* + * See http://bit.ly/2CcoEsr for run-time detection of ARM features and also + * crbug.com/931275 for android_getCpuFeatures() use in the Android sandbox. + */ +static void _cpu_check_features(void) +{ +#if defined(ARMV8_OS_ANDROID) && defined(__aarch64__) + uint64_t features = android_getCpuFeatures(); + arm_cpu_enable_crc32 = !!(features & ANDROID_CPU_ARM64_FEATURE_CRC32); + arm_cpu_enable_pmull = !!(features & ANDROID_CPU_ARM64_FEATURE_PMULL); +#elif defined(ARMV8_OS_ANDROID) /* aarch32 */ + uint64_t features = android_getCpuFeatures(); + arm_cpu_enable_crc32 = !!(features & ANDROID_CPU_ARM_FEATURE_CRC32); + arm_cpu_enable_pmull = !!(features & ANDROID_CPU_ARM_FEATURE_PMULL); +#elif defined(ARMV8_OS_LINUX) && defined(__aarch64__) + unsigned long features = getauxval(AT_HWCAP); + arm_cpu_enable_crc32 = !!(features & HWCAP_CRC32); + arm_cpu_enable_pmull = !!(features & HWCAP_PMULL); +#elif defined(ARMV8_OS_LINUX) && (defined(__ARM_NEON) || defined(__ARM_NEON__)) + /* Query HWCAP2 for ARMV8-A SoCs running in aarch32 mode */ + unsigned long features = getauxval(AT_HWCAP2); + arm_cpu_enable_crc32 = !!(features & HWCAP2_CRC32); + arm_cpu_enable_pmull = !!(features & HWCAP2_PMULL); +#elif defined(ARMV8_OS_FUCHSIA) + uint32_t features; + zx_status_t rc = zx_system_get_features(ZX_FEATURE_KIND_CPU, &features); + if (rc != ZX_OK || (features & ZX_ARM64_FEATURE_ISA_ASIMD) == 0) + return; /* Report nothing if ASIMD(NEON) is missing */ + arm_cpu_enable_crc32 = !!(features & ZX_ARM64_FEATURE_ISA_CRC32); + arm_cpu_enable_pmull = !!(features & ZX_ARM64_FEATURE_ISA_PMULL); +#elif defined(ARMV8_OS_WINDOWS) + arm_cpu_enable_crc32 = IsProcessorFeaturePresent(PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE); + arm_cpu_enable_pmull = IsProcessorFeaturePresent(PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE); +#elif defined(ARMV8_OS_IOS) + // Determine what features are supported dynamically. This code is applicable to macOS + // as well if we wish to do that dynamically on that platform in the future. + // See https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics + int val = 0; + size_t len = sizeof(val); + arm_cpu_enable_crc32 = sysctlbyname("hw.optional.armv8_crc32", &val, &len, 0, 0) == 0 + && val != 0; + val = 0; + len = sizeof(val); + arm_cpu_enable_pmull = sysctlbyname("hw.optional.arm.FEAT_PMULL", &val, &len, 0, 0) == 0 + && val != 0; +#endif +} +#endif +#elif defined(X86_NOT_WINDOWS) || defined(X86_WINDOWS) +/* + * iOS@x86 (i.e. emulator) is another special case where we disable + * SIMD optimizations. + */ +#ifndef CPU_NO_SIMD +/* On x86 we simply use a instruction to check the CPU features. + * (i.e. CPUID). + */ +#ifdef CRC32_SIMD_AVX512_PCLMUL +#include +#include +#endif +/* _xgetbv() below needs the xsave ISA. Annotate it here so the build does not + * need -mxsave. */ +#if defined(CRC32_SIMD_AVX512_PCLMUL) && (defined(__GNUC__) || defined(__clang__)) +__attribute__((__target__("xsave"))) +#endif +static void _cpu_check_features(void) +{ + int x86_cpu_has_sse2; + int x86_cpu_has_ssse3; + int x86_cpu_has_sse42; + int x86_cpu_has_pclmulqdq; + int abcd[4]; + +#ifdef _MSC_VER + __cpuid(abcd, 1); +#else + __cpuid(1, abcd[0], abcd[1], abcd[2], abcd[3]); +#endif + + x86_cpu_has_sse2 = abcd[3] & 0x4000000; + x86_cpu_has_ssse3 = abcd[2] & 0x000200; + x86_cpu_has_sse42 = abcd[2] & 0x100000; + x86_cpu_has_pclmulqdq = abcd[2] & 0x2; + + x86_cpu_enable_sse2 = x86_cpu_has_sse2; + + x86_cpu_enable_ssse3 = x86_cpu_has_ssse3; + + x86_cpu_enable_simd = x86_cpu_has_sse2 && + x86_cpu_has_sse42 && + x86_cpu_has_pclmulqdq; + +#ifdef CRC32_SIMD_AVX512_PCLMUL + x86_cpu_enable_avx512 = _xgetbv(0) & 0x00000040; +#endif +} +#endif // x86 & NO_SIMD + +#elif defined(RISCV_RVV) +#include + +#ifndef ZLIB_HWCAP_RVV +#define ZLIB_HWCAP_RVV (1 << ('v' - 'a')) +#endif + +/* TODO(cavalcantii) + * - add support for Android@RISCV i.e. __riscv_hwprobe(). + * - detect vclmul (crypto extensions). + */ +static void _cpu_check_features(void) +{ + unsigned long features = getauxval(AT_HWCAP); + riscv_cpu_enable_rvv = !!(features & ZLIB_HWCAP_RVV); +} +#endif // ARM | x86 | RISCV +#endif // NO SIMD CPU diff --git a/packages/runtime/vendor/zlib/cpu_features.h b/packages/runtime/vendor/zlib/cpu_features.h new file mode 100644 index 000000000..6092c7e85 --- /dev/null +++ b/packages/runtime/vendor/zlib/cpu_features.h @@ -0,0 +1,22 @@ +/* cpu_features.h -- Processor features detection. + * + * Copyright 2018 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#include "zlib.h" + +/* TODO(cavalcantii): remove checks for x86_flags on deflate. + */ +extern int arm_cpu_enable_crc32; +extern int arm_cpu_enable_pmull; +extern int x86_cpu_enable_sse2; +extern int x86_cpu_enable_ssse3; +extern int x86_cpu_enable_simd; +extern int x86_cpu_enable_avx512; + +extern int riscv_cpu_enable_rvv; +extern int riscv_cpu_enable_vclmul; + +void cpu_check_features(void); diff --git a/packages/runtime/vendor/zlib/crc32.c b/packages/runtime/vendor/zlib/crc32.c index 6c38f5c04..2ec5ab5ce 100644 --- a/packages/runtime/vendor/zlib/crc32.c +++ b/packages/runtime/vendor/zlib/crc32.c @@ -1,5 +1,5 @@ /* crc32.c -- compute the CRC-32 of a data stream - * Copyright (C) 1995-2022 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h * * This interleaved implementation of a CRC makes use of pipelined multiple @@ -24,11 +24,24 @@ # include # ifndef DYNAMIC_CRC_TABLE # define DYNAMIC_CRC_TABLE -# endif /* !DYNAMIC_CRC_TABLE */ -#endif /* MAKECRCH */ +# endif +#endif +#ifdef DYNAMIC_CRC_TABLE +# define Z_ONCE +#endif +#include "deflate.h" +#include "cpu_features.h" #include "zutil.h" /* for Z_U4, Z_U8, z_crc_t, and FAR definitions */ +#ifdef HAVE_S390X_VX +# include "contrib/crc32vx/crc32_vx_hooks.h" +#endif + +#if defined(CRC32_SIMD_SSE42_PCLMUL) || defined(CRC32_ARMV8_CRC32) +#include "crc32_simd.h" +#endif + /* A CRC of a message is computed on N braids of words in the message, where each word consists of W bytes (4 or 8). If N is 3, for example, then three @@ -99,11 +112,12 @@ #endif /* If available, use the ARM processor CRC32 instruction. */ -#if defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) && W == 8 -# define ARMCRC32 +#if defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) && \ + defined(W) && W == 8 && defined(USE_CANONICAL_ARMV8_CRC32) +# define ARMCRC32_CANONICAL_ZLIB #endif -#if defined(W) && (!defined(ARMCRC32) || defined(DYNAMIC_CRC_TABLE)) +#if defined(W) && (!defined(ARMCRC32_CANONICAL_ZLIB) || defined(DYNAMIC_CRC_TABLE)) /* Swap the bytes in a z_word_t to convert between little and big endian. Any self-respecting compiler will optimize this to a single machine byte-swap @@ -152,10 +166,10 @@ local z_word_t byte_swap(z_word_t word) { Return a(x) multiplied by b(x) modulo p(x), where p(x) is the CRC polynomial, reflected. For speed, this requires that a not be zero. */ -local z_crc_t multmodp(z_crc_t a, z_crc_t b) { - z_crc_t m, p; +local uLong multmodp(uLong a, uLong b) { + uLong m, p; - m = (z_crc_t)1 << 31; + m = (uLong)1 << 31; p = 0; for (;;) { if (a & m) { @@ -171,12 +185,12 @@ local z_crc_t multmodp(z_crc_t a, z_crc_t b) { /* Return x^(n * 2^k) modulo p(x). Requires that x2n_table[] has been - initialized. + initialized. n must not be negative. */ -local z_crc_t x2nmodp(z_off64_t n, unsigned k) { - z_crc_t p; +local uLong x2nmodp(z_off64_t n, unsigned k) { + uLong p; - p = (z_crc_t)1 << 31; /* x^0 == 1 */ + p = (uLong)1 << 31; /* x^0 == 1 */ while (n) { if (n & 1) p = multmodp(x2n_table[k & 31], p); @@ -204,83 +218,8 @@ local z_crc_t FAR crc_table[256]; local void write_table64(FILE *, const z_word_t FAR *, int); #endif /* MAKECRCH */ -/* - Define a once() function depending on the availability of atomics. If this is - compiled with DYNAMIC_CRC_TABLE defined, and if CRCs will be computed in - multiple threads, and if atomics are not available, then get_crc_table() must - be called to initialize the tables and must return before any threads are - allowed to compute or combine CRCs. - */ - -/* Definition of once functionality. */ -typedef struct once_s once_t; - -/* Check for the availability of atomics. */ -#if defined(__STDC__) && __STDC_VERSION__ >= 201112L && \ - !defined(__STDC_NO_ATOMICS__) - -#include - -/* Structure for once(), which must be initialized with ONCE_INIT. */ -struct once_s { - atomic_flag begun; - atomic_int done; -}; -#define ONCE_INIT {ATOMIC_FLAG_INIT, 0} - -/* - Run the provided init() function exactly once, even if multiple threads - invoke once() at the same time. The state must be a once_t initialized with - ONCE_INIT. - */ -local void once(once_t *state, void (*init)(void)) { - if (!atomic_load(&state->done)) { - if (atomic_flag_test_and_set(&state->begun)) - while (!atomic_load(&state->done)) - ; - else { - init(); - atomic_store(&state->done, 1); - } - } -} - -#else /* no atomics */ - -/* Structure for once(), which must be initialized with ONCE_INIT. */ -struct once_s { - volatile int begun; - volatile int done; -}; -#define ONCE_INIT {0, 0} - -/* Test and set. Alas, not atomic, but tries to minimize the period of - vulnerability. */ -local int test_and_set(int volatile *flag) { - int was; - - was = *flag; - *flag = 1; - return was; -} - -/* Run the provided init() function once. This is not thread-safe. */ -local void once(once_t *state, void (*init)(void)) { - if (!state->done) { - if (test_and_set(&state->begun)) - while (!state->done) - ; - else { - init(); - state->done = 1; - } - } -} - -#endif - /* State for once(). */ -local once_t made = ONCE_INIT; +local z_once_t made = Z_ONCE_INIT; /* Generate tables for a byte-wise 32-bit CRC calculation on the polynomial: @@ -306,8 +245,8 @@ local once_t made = ONCE_INIT; information needed to generate CRCs on data a byte at a time for all combinations of CRC register values and incoming bytes. */ - -local void make_crc_table(void) { +local void make_crc_table(void) +{ unsigned i, j, n; z_crc_t p; @@ -326,7 +265,7 @@ local void make_crc_table(void) { p = (z_crc_t)1 << 30; /* x^1 */ x2n_table[0] = p; for (n = 1; n < 32; n++) - x2n_table[n] = p = multmodp(p, p); + x2n_table[n] = p = (z_crc_t)multmodp(p, p); #ifdef W /* initialize the braiding tables -- needs x2n_table[] */ @@ -529,11 +468,11 @@ local void braid(z_crc_t ltl[][256], z_word_t big[][256], int n, int w) { int k; z_crc_t i, p, q; for (k = 0; k < w; k++) { - p = x2nmodp((n * w + 3 - k) << 3, 0); + p = (z_crc_t)x2nmodp((n * w + 3 - k) << 3, 0); ltl[k][0] = 0; big[w - 1 - k][0] = 0; for (i = 1; i < 256; i++) { - ltl[k][i] = q = multmodp(i << 24, p); + ltl[k][i] = q = (z_crc_t)multmodp(i << 24, p); big[w - 1 - k][i] = byte_swap(q); } } @@ -548,7 +487,7 @@ local void braid(z_crc_t ltl[][256], z_word_t big[][256], int n, int w) { */ const z_crc_t FAR * ZEXPORT get_crc_table(void) { #ifdef DYNAMIC_CRC_TABLE - once(&made, make_crc_table); + z_once(&made, make_crc_table); #endif /* DYNAMIC_CRC_TABLE */ return (const z_crc_t FAR *)crc_table; } @@ -562,7 +501,7 @@ const z_crc_t FAR * ZEXPORT get_crc_table(void) { * -march=armv8-a+crc, or -march=native if the compile machine has the crc32 * instructions. */ -#ifdef ARMCRC32 +#if ARMCRC32_CANONICAL_ZLIB /* Constants empirically determined to maximize speed. These values are from @@ -572,9 +511,8 @@ const z_crc_t FAR * ZEXPORT get_crc_table(void) { #define Z_BATCH_ZEROS 0xa10d3d0c /* computed from Z_BATCH = 3990 */ #define Z_BATCH_MIN 800 /* fewest words in a final batch */ -unsigned long ZEXPORT crc32_z(unsigned long crc, const unsigned char FAR *buf, - z_size_t len) { - z_crc_t val; +uLong ZEXPORT crc32_z(uLong crc, const unsigned char FAR *buf, z_size_t len) { + uLong val; z_word_t crc1, crc2; const z_word_t *word; z_word_t val0, val1, val2; @@ -585,7 +523,7 @@ unsigned long ZEXPORT crc32_z(unsigned long crc, const unsigned char FAR *buf, if (buf == Z_NULL) return 0; #ifdef DYNAMIC_CRC_TABLE - once(&made, make_crc_table); + z_once(&made, make_crc_table); #endif /* DYNAMIC_CRC_TABLE */ /* Pre-condition the CRC */ @@ -640,7 +578,7 @@ unsigned long ZEXPORT crc32_z(unsigned long crc, const unsigned char FAR *buf, } word += 3 * last; num -= 3 * last; - val = x2nmodp(last, 6); + val = x2nmodp((int)last, 6); crc = multmodp(val, crc) ^ crc1; crc = multmodp(val, crc) ^ crc2; } @@ -691,15 +629,79 @@ local z_word_t crc_word_big(z_word_t data) { #endif /* ========================================================================= */ -unsigned long ZEXPORT crc32_z(unsigned long crc, const unsigned char FAR *buf, - z_size_t len) { - /* Return initial CRC, if requested. */ - if (buf == Z_NULL) return 0; +uLong ZEXPORT crc32_z(uLong crc, const unsigned char FAR *buf, z_size_t len) { + + /* If no optimizations are enabled, do it as canonical zlib. */ +#if !defined(CRC32_SIMD_SSE42_PCLMUL) && !defined(CRC32_ARMV8_CRC32) && \ + !defined(RISCV_RVV) && !defined(CRC32_SIMD_AVX512_PCLMUL) + if (buf == Z_NULL) { + return 0UL; + } +#else + /* + * zlib convention is to call crc32(0, NULL, 0); before making + * calls to crc32(). So this is a good, early (and infrequent) + * place to cache CPU features if needed for those later, more + * interesting crc32() calls. + */ + if (buf == Z_NULL) { + if (!len) + cpu_check_features(); + return 0UL; + } +#endif + /* If AVX-512 is enabled, we will use it for longer inputs and fallback + * to SSE4.2 and eventually the portable implementation to handle the tail. + */ +#if defined(CRC32_SIMD_AVX512_PCLMUL) + if (x86_cpu_enable_avx512 && len >= Z_CRC32_AVX512_MINIMUM_LENGTH) { + /* crc32 64-byte chunks */ + z_size_t chunk_size = len & ~Z_CRC32_AVX512_CHUNKSIZE_MASK; + crc = ~crc32_avx512_simd_(buf, chunk_size, ~(uint32_t)crc); + /* check remaining data */ + len -= chunk_size; + if (!len) + return crc; + /* Fall into the default crc32 for the remaining data. */ + buf += chunk_size; + } +#endif +#if defined(CRC32_SIMD_SSE42_PCLMUL) + if (x86_cpu_enable_simd && len >= Z_CRC32_SSE42_MINIMUM_LENGTH) { + /* crc32 16-byte chunks */ + z_size_t chunk_size = len & ~Z_CRC32_SSE42_CHUNKSIZE_MASK; + crc = ~crc32_sse42_simd_(buf, chunk_size, ~(uint32_t)crc); + /* check remaining data */ + len -= chunk_size; + if (!len) + return crc; + /* Fall into the default crc32 for the remaining data. */ + buf += chunk_size; + } +#elif defined(CRC32_ARMV8_CRC32) + if (arm_cpu_enable_crc32) { +#if defined(__aarch64__) + /* PMULL is 64bit only, plus code needs at least a 64 bytes buffer. */ + if (arm_cpu_enable_pmull && (len > Z_CRC32_PMULL_MINIMUM_LENGTH)) { + const size_t chunk_size = len & ~Z_CRC32_PMULL_CHUNKSIZE_MASK; + crc = ~armv8_crc32_pmull_little(buf, chunk_size, ~(uint32_t)crc); + /* Check remaining data. */ + len -= chunk_size; + if (!len) + return crc; + + /* Fall through for the remaining data. */ + buf += chunk_size; + } +#endif + /* This is scalar and self contained, used on Armv8@32bit or tail. */ + return armv8_crc32_little(buf, len, crc); + } +#endif /* CRC32_SIMD */ #ifdef DYNAMIC_CRC_TABLE - once(&made, make_crc_table); + z_once(&made, make_crc_table); #endif /* DYNAMIC_CRC_TABLE */ - /* Pre-condition the CRC */ crc = (~crc) & 0xffffffff; @@ -1012,28 +1014,56 @@ unsigned long ZEXPORT crc32_z(unsigned long crc, const unsigned char FAR *buf, #endif /* ========================================================================= */ -unsigned long ZEXPORT crc32(unsigned long crc, const unsigned char FAR *buf, - uInt len) { - return crc32_z(crc, buf, len); -} +uLong ZEXPORT crc32(uLong crc, const unsigned char FAR *buf, uInt len) { + #ifdef HAVE_S390X_VX + return crc32_z_hook(crc, buf, len); + #endif -/* ========================================================================= */ -uLong ZEXPORT crc32_combine64(uLong crc1, uLong crc2, z_off64_t len2) { -#ifdef DYNAMIC_CRC_TABLE - once(&made, make_crc_table); -#endif /* DYNAMIC_CRC_TABLE */ - return multmodp(x2nmodp(len2, 3), crc1) ^ (crc2 & 0xffffffff); -} + /* Some bots compile with optimizations disabled, others will emulate + * ARM on x86 and other weird combinations. + */ +#if defined(CRC32_SIMD_SSE42_PCLMUL) || defined(CRC32_ARMV8_CRC32) \ + || defined(RISCV_RVV) + /* We got to verify CPU features, so exploit the common usage pattern + * of calling this function with Z_NULL for an initial valid crc value. + * This allows to cache the result of the feature check and avoid extraneous + * function calls. + */ + if (buf == Z_NULL) { + if (!len) /* Assume user is calling crc32(0, NULL, 0); */ + cpu_check_features(); + return 0UL; + } +#endif -/* ========================================================================= */ -uLong ZEXPORT crc32_combine(uLong crc1, uLong crc2, z_off_t len2) { - return crc32_combine64(crc1, crc2, (z_off64_t)len2); +#if defined(CRC32_ARMV8_CRC32) + if (arm_cpu_enable_crc32) { +#if defined(__aarch64__) + /* PMULL is 64bit only, plus code needs at least a 64 bytes buffer. */ + if (arm_cpu_enable_pmull && (len > Z_CRC32_PMULL_MINIMUM_LENGTH)) { + const size_t chunk_size = len & ~Z_CRC32_PMULL_CHUNKSIZE_MASK; + crc = ~armv8_crc32_pmull_little(buf, chunk_size, ~(uint32_t)crc); + /* Check remaining data. */ + len -= chunk_size; + if (!len) + return crc; + + /* Fall through for the remaining data. */ + buf += chunk_size; + } +#endif + return armv8_crc32_little(buf, len, crc); /* Armv8@32bit or tail. */ + } +#endif + return crc32_z(crc, buf, len); /* Armv7 or Armv8 w/o crypto extensions. */ } /* ========================================================================= */ uLong ZEXPORT crc32_combine_gen64(z_off64_t len2) { + if (len2 < 0) + return 0; #ifdef DYNAMIC_CRC_TABLE - once(&made, make_crc_table); + z_once(&made, make_crc_table); #endif /* DYNAMIC_CRC_TABLE */ return x2nmodp(len2, 3); } @@ -1042,8 +1072,55 @@ uLong ZEXPORT crc32_combine_gen64(z_off64_t len2) { uLong ZEXPORT crc32_combine_gen(z_off_t len2) { return crc32_combine_gen64((z_off64_t)len2); } - /* ========================================================================= */ uLong ZEXPORT crc32_combine_op(uLong crc1, uLong crc2, uLong op) { - return multmodp(op, crc1) ^ (crc2 & 0xffffffff); + if (op == 0) + return 0; + return multmodp(op, crc1 & 0xffffffff) ^ (crc2 & 0xffffffff); +} + +/* ========================================================================= */ +uLong ZEXPORT crc32_combine64(uLong crc1, uLong crc2, z_off64_t len2) { + return crc32_combine_op(crc1, crc2, crc32_combine_gen64(len2)); +} + +/* ========================================================================= */ +uLong ZEXPORT crc32_combine(uLong crc1, uLong crc2, z_off_t len2) { + return crc32_combine64(crc1, crc2, (z_off64_t)len2); +} + +ZLIB_INTERNAL void crc_reset(deflate_state *const s) +{ +#ifdef CRC32_SIMD_SSE42_PCLMUL + if (x86_cpu_enable_simd) { + crc_fold_init(s); + return; + } +#endif + s->strm->adler = crc32(0L, Z_NULL, 0); +} + +ZLIB_INTERNAL void crc_finalize(deflate_state *const s) +{ +#ifdef QAT_COMPRESSION_ENABLED + if (s->qat_s) { + return; + } +#endif +#ifdef CRC32_SIMD_SSE42_PCLMUL + if (x86_cpu_enable_simd) + s->strm->adler = crc_fold_512to32(s); +#endif +} + +ZLIB_INTERNAL void copy_with_crc(z_streamp strm, Bytef *dst, long size) +{ +#ifdef CRC32_SIMD_SSE42_PCLMUL + if (x86_cpu_enable_simd) { + crc_fold_copy(strm->state, dst, strm->next_in, size); + return; + } +#endif + zmemcpy(dst, strm->next_in, size); + strm->adler = crc32(strm->adler, dst, size); } diff --git a/packages/runtime/vendor/zlib/crc32_simd.c b/packages/runtime/vendor/zlib/crc32_simd.c new file mode 100644 index 000000000..f9d52a631 --- /dev/null +++ b/packages/runtime/vendor/zlib/crc32_simd.c @@ -0,0 +1,628 @@ +/* crc32_simd.c + * + * Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#include "crc32_simd.h" +#if defined(CRC32_SIMD_AVX512_PCLMUL) + +/* + * crc32_avx512_simd_(): compute the crc32 of the buffer, where the buffer + * length must be at least 256, and a multiple of 64. Based on: + * + * "Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction" + * V. Gopal, E. Ozturk, et al., 2009, http://intel.ly/2ySEwL0 + */ + +#include +#include +#include +#include + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((__target__("avx512f,pclmul,vpclmulqdq"))) +#endif +uint32_t ZLIB_INTERNAL crc32_avx512_simd_( /* AVX512+PCLMUL */ + const unsigned char *buf, + z_size_t len, + uint32_t crc) +{ + /* + * Definitions of the bit-reflected domain constants k1,k2,k3,k4 + * are similar to those given at the end of the paper, and remaining + * constants and CRC32+Barrett polynomials remain unchanged. + * + * Replace the index of x from 128 to 512. As follows: + * k1 = ( x ^ ( 512 * 4 + 32 ) mod P(x) << 32 )' << 1 = 0x011542778a + * k2 = ( x ^ ( 512 * 4 - 32 ) mod P(x) << 32 )' << 1 = 0x01322d1430 + * k3 = ( x ^ ( 512 + 32 ) mod P(x) << 32 )' << 1 = 0x0154442bd4 + * k4 = ( x ^ ( 512 - 32 ) mod P(x) << 32 )' << 1 = 0x01c6e41596 + */ + static const uint64_t zalign(64) k1k2[] = { 0x011542778a, 0x01322d1430, + 0x011542778a, 0x01322d1430, + 0x011542778a, 0x01322d1430, + 0x011542778a, 0x01322d1430 }; + static const uint64_t zalign(64) k3k4[] = { 0x0154442bd4, 0x01c6e41596, + 0x0154442bd4, 0x01c6e41596, + 0x0154442bd4, 0x01c6e41596, + 0x0154442bd4, 0x01c6e41596 }; + static const uint64_t zalign(16) k5k6[] = { 0x01751997d0, 0x00ccaa009e }; + static const uint64_t zalign(16) k7k8[] = { 0x0163cd6124, 0x0000000000 }; + static const uint64_t zalign(16) poly[] = { 0x01db710641, 0x01f7011641 }; + __m512i x0, x1, x2, x3, x4, x5, x6, x7, x8, y5, y6, y7, y8; + __m128i a0, a1, a2, a3; + + /* + * There's at least one block of 256. + */ + x1 = _mm512_loadu_si512((__m512i *)(buf + 0x00)); + x2 = _mm512_loadu_si512((__m512i *)(buf + 0x40)); + x3 = _mm512_loadu_si512((__m512i *)(buf + 0x80)); + x4 = _mm512_loadu_si512((__m512i *)(buf + 0xC0)); + + x1 = _mm512_xor_si512(x1, _mm512_castsi128_si512(_mm_cvtsi32_si128(crc))); + + x0 = _mm512_load_si512((__m512i *)k1k2); + + buf += 256; + len -= 256; + + /* + * Parallel fold blocks of 256, if any. + */ + while (len >= 256) + { + x5 = _mm512_clmulepi64_epi128(x1, x0, 0x00); + x6 = _mm512_clmulepi64_epi128(x2, x0, 0x00); + x7 = _mm512_clmulepi64_epi128(x3, x0, 0x00); + x8 = _mm512_clmulepi64_epi128(x4, x0, 0x00); + + + x1 = _mm512_clmulepi64_epi128(x1, x0, 0x11); + x2 = _mm512_clmulepi64_epi128(x2, x0, 0x11); + x3 = _mm512_clmulepi64_epi128(x3, x0, 0x11); + x4 = _mm512_clmulepi64_epi128(x4, x0, 0x11); + + y5 = _mm512_loadu_si512((__m512i *)(buf + 0x00)); + y6 = _mm512_loadu_si512((__m512i *)(buf + 0x40)); + y7 = _mm512_loadu_si512((__m512i *)(buf + 0x80)); + y8 = _mm512_loadu_si512((__m512i *)(buf + 0xC0)); + + x1 = _mm512_xor_si512(x1, x5); + x2 = _mm512_xor_si512(x2, x6); + x3 = _mm512_xor_si512(x3, x7); + x4 = _mm512_xor_si512(x4, x8); + + x1 = _mm512_xor_si512(x1, y5); + x2 = _mm512_xor_si512(x2, y6); + x3 = _mm512_xor_si512(x3, y7); + x4 = _mm512_xor_si512(x4, y8); + + buf += 256; + len -= 256; + } + + /* + * Fold into 512-bits. + */ + x0 = _mm512_load_si512((__m512i *)k3k4); + + x5 = _mm512_clmulepi64_epi128(x1, x0, 0x00); + x1 = _mm512_clmulepi64_epi128(x1, x0, 0x11); + x1 = _mm512_xor_si512(x1, x2); + x1 = _mm512_xor_si512(x1, x5); + + x5 = _mm512_clmulepi64_epi128(x1, x0, 0x00); + x1 = _mm512_clmulepi64_epi128(x1, x0, 0x11); + x1 = _mm512_xor_si512(x1, x3); + x1 = _mm512_xor_si512(x1, x5); + + x5 = _mm512_clmulepi64_epi128(x1, x0, 0x00); + x1 = _mm512_clmulepi64_epi128(x1, x0, 0x11); + x1 = _mm512_xor_si512(x1, x4); + x1 = _mm512_xor_si512(x1, x5); + + /* + * Single fold blocks of 64, if any. + */ + while (len >= 64) + { + x2 = _mm512_loadu_si512((__m512i *)buf); + + x5 = _mm512_clmulepi64_epi128(x1, x0, 0x00); + x1 = _mm512_clmulepi64_epi128(x1, x0, 0x11); + x1 = _mm512_xor_si512(x1, x2); + x1 = _mm512_xor_si512(x1, x5); + + buf += 64; + len -= 64; + } + + /* + * Fold 512-bits to 384-bits. + */ + a0 = _mm_load_si128((__m128i *)k5k6); + + a1 = _mm512_extracti32x4_epi32(x1, 0); + a2 = _mm512_extracti32x4_epi32(x1, 1); + + a3 = _mm_clmulepi64_si128(a1, a0, 0x00); + a1 = _mm_clmulepi64_si128(a1, a0, 0x11); + + a1 = _mm_xor_si128(a1, a3); + a1 = _mm_xor_si128(a1, a2); + + /* + * Fold 384-bits to 256-bits. + */ + a2 = _mm512_extracti32x4_epi32(x1, 2); + a3 = _mm_clmulepi64_si128(a1, a0, 0x00); + a1 = _mm_clmulepi64_si128(a1, a0, 0x11); + a1 = _mm_xor_si128(a1, a3); + a1 = _mm_xor_si128(a1, a2); + + /* + * Fold 256-bits to 128-bits. + */ + a2 = _mm512_extracti32x4_epi32(x1, 3); + a3 = _mm_clmulepi64_si128(a1, a0, 0x00); + a1 = _mm_clmulepi64_si128(a1, a0, 0x11); + a1 = _mm_xor_si128(a1, a3); + a1 = _mm_xor_si128(a1, a2); + + /* + * Fold 128-bits to 64-bits. + */ + a2 = _mm_clmulepi64_si128(a1, a0, 0x10); + a3 = _mm_setr_epi32(~0, 0, ~0, 0); + a1 = _mm_srli_si128(a1, 8); + a1 = _mm_xor_si128(a1, a2); + + a0 = _mm_loadl_epi64((__m128i*)k7k8); + a2 = _mm_srli_si128(a1, 4); + a1 = _mm_and_si128(a1, a3); + a1 = _mm_clmulepi64_si128(a1, a0, 0x00); + a1 = _mm_xor_si128(a1, a2); + + /* + * Barret reduce to 32-bits. + */ + a0 = _mm_load_si128((__m128i*)poly); + + a2 = _mm_and_si128(a1, a3); + a2 = _mm_clmulepi64_si128(a2, a0, 0x10); + a2 = _mm_and_si128(a2, a3); + a2 = _mm_clmulepi64_si128(a2, a0, 0x00); + a1 = _mm_xor_si128(a1, a2); + + /* + * Return the crc32. + */ + return _mm_extract_epi32(a1, 1); +} + +#endif +#if defined(CRC32_SIMD_SSE42_PCLMUL) + +/* + * crc32_sse42_simd_(): compute the crc32 of the buffer, where the buffer + * length must be at least 64, and a multiple of 16. + */ + +#include +#include +#include + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((__target__("sse4.2,pclmul"))) +#endif +uint32_t ZLIB_INTERNAL crc32_sse42_simd_( /* SSE4.2+PCLMUL */ + const unsigned char *buf, + z_size_t len, + uint32_t crc) +{ + /* + * Definitions of the bit-reflected domain constants k1,k2,k3, etc and + * the CRC32+Barrett polynomials given at the end of the paper. + */ + static const uint64_t zalign(16) k1k2[] = { 0x0154442bd4, 0x01c6e41596 }; + static const uint64_t zalign(16) k3k4[] = { 0x01751997d0, 0x00ccaa009e }; + static const uint64_t zalign(16) k5k0[] = { 0x0163cd6124, 0x0000000000 }; + static const uint64_t zalign(16) poly[] = { 0x01db710641, 0x01f7011641 }; + + __m128i x0, x1, x2, x3, x4, x5, x6, x7, x8, y5, y6, y7, y8; + + /* + * There's at least one block of 64. + */ + x1 = _mm_loadu_si128((__m128i *)(buf + 0x00)); + x2 = _mm_loadu_si128((__m128i *)(buf + 0x10)); + x3 = _mm_loadu_si128((__m128i *)(buf + 0x20)); + x4 = _mm_loadu_si128((__m128i *)(buf + 0x30)); + + x1 = _mm_xor_si128(x1, _mm_cvtsi32_si128(crc)); + + x0 = _mm_load_si128((__m128i *)k1k2); + + buf += 64; + len -= 64; + + /* + * Parallel fold blocks of 64, if any. + */ + while (len >= 64) + { + x5 = _mm_clmulepi64_si128(x1, x0, 0x00); + x6 = _mm_clmulepi64_si128(x2, x0, 0x00); + x7 = _mm_clmulepi64_si128(x3, x0, 0x00); + x8 = _mm_clmulepi64_si128(x4, x0, 0x00); + + x1 = _mm_clmulepi64_si128(x1, x0, 0x11); + x2 = _mm_clmulepi64_si128(x2, x0, 0x11); + x3 = _mm_clmulepi64_si128(x3, x0, 0x11); + x4 = _mm_clmulepi64_si128(x4, x0, 0x11); + + y5 = _mm_loadu_si128((__m128i *)(buf + 0x00)); + y6 = _mm_loadu_si128((__m128i *)(buf + 0x10)); + y7 = _mm_loadu_si128((__m128i *)(buf + 0x20)); + y8 = _mm_loadu_si128((__m128i *)(buf + 0x30)); + + x1 = _mm_xor_si128(x1, x5); + x2 = _mm_xor_si128(x2, x6); + x3 = _mm_xor_si128(x3, x7); + x4 = _mm_xor_si128(x4, x8); + + x1 = _mm_xor_si128(x1, y5); + x2 = _mm_xor_si128(x2, y6); + x3 = _mm_xor_si128(x3, y7); + x4 = _mm_xor_si128(x4, y8); + + buf += 64; + len -= 64; + } + + /* + * Fold into 128-bits. + */ + x0 = _mm_load_si128((__m128i *)k3k4); + + x5 = _mm_clmulepi64_si128(x1, x0, 0x00); + x1 = _mm_clmulepi64_si128(x1, x0, 0x11); + x1 = _mm_xor_si128(x1, x2); + x1 = _mm_xor_si128(x1, x5); + + x5 = _mm_clmulepi64_si128(x1, x0, 0x00); + x1 = _mm_clmulepi64_si128(x1, x0, 0x11); + x1 = _mm_xor_si128(x1, x3); + x1 = _mm_xor_si128(x1, x5); + + x5 = _mm_clmulepi64_si128(x1, x0, 0x00); + x1 = _mm_clmulepi64_si128(x1, x0, 0x11); + x1 = _mm_xor_si128(x1, x4); + x1 = _mm_xor_si128(x1, x5); + + /* + * Single fold blocks of 16, if any. + */ + while (len >= 16) + { + x2 = _mm_loadu_si128((__m128i *)buf); + + x5 = _mm_clmulepi64_si128(x1, x0, 0x00); + x1 = _mm_clmulepi64_si128(x1, x0, 0x11); + x1 = _mm_xor_si128(x1, x2); + x1 = _mm_xor_si128(x1, x5); + + buf += 16; + len -= 16; + } + + /* + * Fold 128-bits to 64-bits. + */ + x2 = _mm_clmulepi64_si128(x1, x0, 0x10); + x3 = _mm_setr_epi32(~0, 0, ~0, 0); + x1 = _mm_srli_si128(x1, 8); + x1 = _mm_xor_si128(x1, x2); + + x0 = _mm_loadl_epi64((__m128i*)k5k0); + + x2 = _mm_srli_si128(x1, 4); + x1 = _mm_and_si128(x1, x3); + x1 = _mm_clmulepi64_si128(x1, x0, 0x00); + x1 = _mm_xor_si128(x1, x2); + + /* + * Barret reduce to 32-bits. + */ + x0 = _mm_load_si128((__m128i*)poly); + + x2 = _mm_and_si128(x1, x3); + x2 = _mm_clmulepi64_si128(x2, x0, 0x10); + x2 = _mm_and_si128(x2, x3); + x2 = _mm_clmulepi64_si128(x2, x0, 0x00); + x1 = _mm_xor_si128(x1, x2); + + /* + * Return the crc32. + */ + return _mm_extract_epi32(x1, 1); +} + +#elif defined(CRC32_ARMV8_CRC32) + +/* CRC32 checksums using ARMv8-a crypto instructions. + */ + +#if defined(__clang__) +/* We need some extra types for using PMULL. + */ +#if defined(__aarch64__) +#include +#include +#endif + +/* CRC32 intrinsics are #ifdef'ed out of arm_acle.h unless we build with an + * armv8 target, which is incompatible with ThinLTO optimizations on Android. + * (Namely, mixing and matching different module-level targets makes ThinLTO + * warn, and Android defaults to armv7-a. This restriction does not apply to + * function-level `target`s, however.) + * + * Since we only need four crc intrinsics, and since clang's implementation of + * those are just wrappers around compiler builtins, it's simplest to #define + * those builtins directly. If this #define list grows too much (or we depend on + * an intrinsic that isn't a trivial wrapper), we may have to find a better way + * to go about this. + * + * NOTE: clang currently complains that "'+soft-float-abi' is not a recognized + * feature for this target (ignoring feature)." This appears to be a harmless + * bug in clang. + * + * These definitions must appear *after* including arm_acle.h otherwise that + * header may end up defining functions named __builtin_arm_crc32* that call + * themselves, creating an infinite loop when the intrinsic is called. + */ +/* XXX: Cannot hook into builtins with XCode for arm64. */ +#if !defined(ARMV8_OS_MACOS) +#define __crc32b __builtin_arm_crc32b +#define __crc32d __builtin_arm_crc32d +#define __crc32w __builtin_arm_crc32w +#define __crc32cw __builtin_arm_crc32cw +#endif + +#if defined(__aarch64__) +#define TARGET_ARMV8_WITH_CRC __attribute__((target("arch=armv8-a+aes+crc"))) +#else // !defined(__aarch64__) +#define TARGET_ARMV8_WITH_CRC __attribute__((target("crc"))) +#endif // defined(__aarch64__) + +#elif defined(__GNUC__) +/* For GCC, we are setting CRC extensions at module level, so ThinLTO is not + * allowed. We can just include arm_acle.h. + */ +#include +#include +#define TARGET_ARMV8_WITH_CRC __attribute__((target("arch=armv8-a+crc+crypto"))) +#else // !defined(__GNUC__) && !defined(_aarch64__) +#error ARM CRC32 SIMD extensions only supported for Clang and GCC +#endif + +TARGET_ARMV8_WITH_CRC +uint32_t ZLIB_INTERNAL armv8_crc32_little( + const unsigned char *buf, + z_size_t len, + uint32_t crc) +{ + uint32_t c = (uint32_t) ~crc; + + while (len && ((uintptr_t)buf & 7)) { + c = __crc32b(c, *buf++); + --len; + } + + const uint64_t *buf8 = (const uint64_t *)buf; + + while (len >= 64) { + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + c = __crc32d(c, *buf8++); + len -= 64; + } + + while (len >= 8) { + c = __crc32d(c, *buf8++); + len -= 8; + } + + buf = (const unsigned char *)buf8; + + while (len--) { + c = __crc32b(c, *buf++); + } + + return ~c; +} + +#if defined(__aarch64__) || defined(ARMV8_OS_MACOS) /* aarch64 specific code. */ + +/* + * crc32_pmull_simd_(): compute the crc32 of the buffer, where the buffer + * length must be at least 64, and a multiple of 16. Based on: + * + * "Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction" + * V. Gopal, E. Ozturk, et al., 2009, http://intel.ly/2ySEwL0 + */ +TARGET_ARMV8_WITH_CRC +static inline uint8x16_t pmull_lo(const uint64x2_t a, const uint64x2_t b) +{ + uint8x16_t r; + __asm__ __volatile__ ("pmull %0.1q, %1.1d, %2.1d \n\t" + : "=w" (r) : "w" (a), "w" (b) ); + return r; +} + +TARGET_ARMV8_WITH_CRC +static inline uint8x16_t pmull_01(const uint64x2_t a, const uint64x2_t b) +{ + uint8x16_t r; + __asm__ __volatile__ ("pmull %0.1q, %1.1d, %2.1d \n\t" + : "=w" (r) : "w" (a), "w" (vgetq_lane_u64(b, 1)) ); + return r; +} + +TARGET_ARMV8_WITH_CRC +static inline uint8x16_t pmull_hi(const uint64x2_t a, const uint64x2_t b) +{ + uint8x16_t r; + __asm__ __volatile__ ("pmull2 %0.1q, %1.2d, %2.2d \n\t" + : "=w" (r) : "w" (a), "w" (b) ); + return r; +} + +TARGET_ARMV8_WITH_CRC +uint32_t ZLIB_INTERNAL armv8_crc32_pmull_little( + const unsigned char *buf, + z_size_t len, + uint32_t crc) +{ + /* + * Definitions of the bit-reflected domain constants k1,k2,k3, etc and + * the CRC32+Barrett polynomials given at the end of the paper. + */ + static const uint64_t zalign(16) k1k2[] = { 0x0154442bd4, 0x01c6e41596 }; + static const uint64_t zalign(16) k3k4[] = { 0x01751997d0, 0x00ccaa009e }; + static const uint64_t zalign(16) k5k0[] = { 0x0163cd6124, 0x0000000000 }; + static const uint64_t zalign(16) poly[] = { 0x01db710641, 0x01f7011641 }; + + uint64x2_t x0, x1, x2, x3, x4, x5, x6, x7, x8, y5, y6, y7, y8; + + /* + * There's at least one block of 64. + */ + x1 = vld1q_u64((const uint64_t *)(buf + 0x00)); + x2 = vld1q_u64((const uint64_t *)(buf + 0x10)); + x3 = vld1q_u64((const uint64_t *)(buf + 0x20)); + x4 = vld1q_u64((const uint64_t *)(buf + 0x30)); + + x1 = veorq_u64(x1, (uint64x2_t) vsetq_lane_u32(crc, vdupq_n_u32(0), 0)); + + x0 = vld1q_u64(k1k2); + + buf += 64; + len -= 64; + + /* + * Parallel fold blocks of 64, if any. + */ + while (len >= 64) + { + x5 = (uint64x2_t) pmull_lo(x1, x0); + x6 = (uint64x2_t) pmull_lo(x2, x0); + x7 = (uint64x2_t) pmull_lo(x3, x0); + x8 = (uint64x2_t) pmull_lo(x4, x0); + + y5 = vld1q_u64((const uint64_t *)(buf + 0x00)); + y6 = vld1q_u64((const uint64_t *)(buf + 0x10)); + y7 = vld1q_u64((const uint64_t *)(buf + 0x20)); + y8 = vld1q_u64((const uint64_t *)(buf + 0x30)); + + x1 = (uint64x2_t) pmull_hi(x1, x0); + x2 = (uint64x2_t) pmull_hi(x2, x0); + x3 = (uint64x2_t) pmull_hi(x3, x0); + x4 = (uint64x2_t) pmull_hi(x4, x0); + + x1 = veorq_u64(x1, x5); + x2 = veorq_u64(x2, x6); + x3 = veorq_u64(x3, x7); + x4 = veorq_u64(x4, x8); + + x1 = veorq_u64(x1, y5); + x2 = veorq_u64(x2, y6); + x3 = veorq_u64(x3, y7); + x4 = veorq_u64(x4, y8); + + buf += 64; + len -= 64; + } + + /* + * Fold into 128-bits. + */ + x0 = vld1q_u64(k3k4); + + x5 = (uint64x2_t) pmull_lo(x1, x0); + x1 = (uint64x2_t) pmull_hi(x1, x0); + x1 = veorq_u64(x1, x2); + x1 = veorq_u64(x1, x5); + + x5 = (uint64x2_t) pmull_lo(x1, x0); + x1 = (uint64x2_t) pmull_hi(x1, x0); + x1 = veorq_u64(x1, x3); + x1 = veorq_u64(x1, x5); + + x5 = (uint64x2_t) pmull_lo(x1, x0); + x1 = (uint64x2_t) pmull_hi(x1, x0); + x1 = veorq_u64(x1, x4); + x1 = veorq_u64(x1, x5); + + /* + * Single fold blocks of 16, if any. + */ + while (len >= 16) + { + x2 = vld1q_u64((const uint64_t *)buf); + + x5 = (uint64x2_t) pmull_lo(x1, x0); + x1 = (uint64x2_t) pmull_hi(x1, x0); + x1 = veorq_u64(x1, x2); + x1 = veorq_u64(x1, x5); + + buf += 16; + len -= 16; + } + + /* + * Fold 128-bits to 64-bits. + */ + static uint32_t zalign(16) mask[] = { ~0u, 0u, ~0u, 0u }; + + x2 = (uint64x2_t) pmull_01(x1, x0); + x1 = (uint64x2_t) vextq_u8(vreinterpretq_u8_u64(x1), vdupq_n_u8(0), 8); + x3 = (uint64x2_t) vld1q_u32(mask); + x1 = veorq_u64(x1, x2); + + x0 = vld1q_u64(k5k0); + + x2 = (uint64x2_t) pmull_01(x2, x0); + x2 = (uint64x2_t) vextq_u8(vreinterpretq_u8_u64(x1), vdupq_n_u8(0), 4); + x1 = vandq_u64(x1, x3); + x1 = (uint64x2_t) pmull_lo(x1, x0); + x1 = veorq_u64(x1, x2); + + /* + * Barret reduce to 32-bits. + */ + x0 = vld1q_u64(poly); + + x2 = vandq_u64(x1, x3); + x2 = (uint64x2_t) pmull_01(x2, x0); + x2 = vandq_u64(x2, x3); + x2 = (uint64x2_t) pmull_lo(x2, x0); + x1 = veorq_u64(x1, x2); + + /* + * Return the crc32. + */ + return vgetq_lane_u32(vreinterpretq_u32_u64(x1), 1); +} +#endif /* aarch64 specific code. */ + +#endif diff --git a/packages/runtime/vendor/zlib/crc32_simd.h b/packages/runtime/vendor/zlib/crc32_simd.h new file mode 100644 index 000000000..84624647b --- /dev/null +++ b/packages/runtime/vendor/zlib/crc32_simd.h @@ -0,0 +1,57 @@ +/* crc32_simd.h + * + * Copyright 2017 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#include + +#include "zconf.h" +#include "zutil.h" +#include "deflate.h" + +/* + * crc32_sse42_simd_(): compute the crc32 of the buffer, where the buffer + * length must be at least 64, and a multiple of 16. + */ +uint32_t ZLIB_INTERNAL crc32_sse42_simd_(const unsigned char* buf, + z_size_t len, + uint32_t crc); + +uint32_t ZLIB_INTERNAL crc32_avx512_simd_(const unsigned char* buf, + z_size_t len, + uint32_t crc); + +/* + * crc32_sse42_simd_ buffer size constraints: see the use in zlib/crc32.c + * for computing the crc32 of an arbitrary length buffer. + */ +#define Z_CRC32_SSE42_MINIMUM_LENGTH 64 +#define Z_CRC32_SSE42_CHUNKSIZE_MASK 15 +#define Z_CRC32_AVX512_MINIMUM_LENGTH 256 +#define Z_CRC32_AVX512_CHUNKSIZE_MASK 63 + +/* + * CRC32 checksums using ARMv8-a crypto instructions. + */ +uint32_t ZLIB_INTERNAL armv8_crc32_little(const unsigned char* buf, + z_size_t len, + uint32_t crc); + +/* aarch64 specific code. */ +#if defined(__aarch64__) + +/* 128 is the sweet spot at the time of coding (late 2020). */ +#define Z_CRC32_PMULL_MINIMUM_LENGTH 128 +#define Z_CRC32_PMULL_CHUNKSIZE_MASK 15 + +/* + * CRC32 checksums using ARMv8-a PMULL instructions, where the buffer + * length must be at least 64, and a multiple of 16. + */ +uint32_t ZLIB_INTERNAL armv8_crc32_pmull_little(const unsigned char* buf, + z_size_t len, + uint32_t crc); + +#endif diff --git a/packages/runtime/vendor/zlib/crc_folding.c b/packages/runtime/vendor/zlib/crc_folding.c new file mode 100644 index 000000000..3e31ae4a7 --- /dev/null +++ b/packages/runtime/vendor/zlib/crc_folding.c @@ -0,0 +1,513 @@ +/* + * Compute the CRC32 using a parallelized folding approach with the PCLMULQDQ + * instruction. + * + * A white paper describing this algorithm can be found at: + * http://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf + * + * Copyright (C) 2013 Intel Corporation. All rights reserved. + * Authors: + * Wajdi Feghali + * Jim Guilford + * Vinodh Gopal + * Erdinc Ozturk + * Jim Kukunas + * + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +#include "deflate.h" + +#ifdef CRC32_SIMD_SSE42_PCLMUL + +#include +#include +#include +#include + +#if defined(__GNUC__) || defined(__clang__) +#define TARGET_SSE42_PCLMUL __attribute__((__target__("sse4.2,pclmul"))) +#else +#define TARGET_SSE42_PCLMUL +#endif + +#define CRC_LOAD(s) \ + do { \ + __m128i xmm_crc0 = _mm_loadu_si128((__m128i *)s->crc0 + 0);\ + __m128i xmm_crc1 = _mm_loadu_si128((__m128i *)s->crc0 + 1);\ + __m128i xmm_crc2 = _mm_loadu_si128((__m128i *)s->crc0 + 2);\ + __m128i xmm_crc3 = _mm_loadu_si128((__m128i *)s->crc0 + 3);\ + __m128i xmm_crc_part = _mm_loadu_si128((__m128i *)s->crc0 + 4); + +#define CRC_SAVE(s) \ + _mm_storeu_si128((__m128i *)s->crc0 + 0, xmm_crc0);\ + _mm_storeu_si128((__m128i *)s->crc0 + 1, xmm_crc1);\ + _mm_storeu_si128((__m128i *)s->crc0 + 2, xmm_crc2);\ + _mm_storeu_si128((__m128i *)s->crc0 + 3, xmm_crc3);\ + _mm_storeu_si128((__m128i *)s->crc0 + 4, xmm_crc_part);\ + } while (0); + +TARGET_SSE42_PCLMUL +ZLIB_INTERNAL void crc_fold_init(deflate_state *const s) +{ + CRC_LOAD(s) + + xmm_crc0 = _mm_cvtsi32_si128(0x9db42487); + xmm_crc1 = _mm_setzero_si128(); + xmm_crc2 = _mm_setzero_si128(); + xmm_crc3 = _mm_setzero_si128(); + + CRC_SAVE(s) + + s->strm->adler = 0; +} + +TARGET_SSE42_PCLMUL +local void fold_1(deflate_state *const s, + __m128i *xmm_crc0, __m128i *xmm_crc1, + __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3; + __m128 ps_crc0, ps_crc3, ps_res; + + x_tmp3 = *xmm_crc3; + + *xmm_crc3 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_res = _mm_xor_ps(ps_crc0, ps_crc3); + + *xmm_crc0 = *xmm_crc1; + *xmm_crc1 = *xmm_crc2; + *xmm_crc2 = x_tmp3; + *xmm_crc3 = _mm_castps_si128(ps_res); +} + +TARGET_SSE42_PCLMUL +local void fold_2(deflate_state *const s, + __m128i *xmm_crc0, __m128i *xmm_crc1, + __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3, x_tmp2; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3, ps_res31, ps_res20; + + x_tmp3 = *xmm_crc3; + x_tmp2 = *xmm_crc2; + + *xmm_crc3 = *xmm_crc1; + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_res31= _mm_xor_ps(ps_crc3, ps_crc1); + + *xmm_crc2 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_res20= _mm_xor_ps(ps_crc0, ps_crc2); + + *xmm_crc0 = x_tmp2; + *xmm_crc1 = x_tmp3; + *xmm_crc2 = _mm_castps_si128(ps_res20); + *xmm_crc3 = _mm_castps_si128(ps_res31); +} + +TARGET_SSE42_PCLMUL +local void fold_3(deflate_state *const s, + __m128i *xmm_crc0, __m128i *xmm_crc1, + __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp3; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3, ps_res32, ps_res21, ps_res10; + + x_tmp3 = *xmm_crc3; + + *xmm_crc3 = *xmm_crc2; + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x01); + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x10); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_res32 = _mm_xor_ps(ps_crc2, ps_crc3); + + *xmm_crc2 = *xmm_crc1; + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x10); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_res21= _mm_xor_ps(ps_crc1, ps_crc2); + + *xmm_crc1 = *xmm_crc0; + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_res10= _mm_xor_ps(ps_crc0, ps_crc1); + + *xmm_crc0 = x_tmp3; + *xmm_crc1 = _mm_castps_si128(ps_res10); + *xmm_crc2 = _mm_castps_si128(ps_res21); + *xmm_crc3 = _mm_castps_si128(ps_res32); +} + +TARGET_SSE42_PCLMUL +local void fold_4(deflate_state *const s, + __m128i *xmm_crc0, __m128i *xmm_crc1, + __m128i *xmm_crc2, __m128i *xmm_crc3) +{ + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + + __m128i x_tmp0, x_tmp1, x_tmp2, x_tmp3; + __m128 ps_crc0, ps_crc1, ps_crc2, ps_crc3; + __m128 ps_t0, ps_t1, ps_t2, ps_t3; + __m128 ps_res0, ps_res1, ps_res2, ps_res3; + + x_tmp0 = *xmm_crc0; + x_tmp1 = *xmm_crc1; + x_tmp2 = *xmm_crc2; + x_tmp3 = *xmm_crc3; + + *xmm_crc0 = _mm_clmulepi64_si128(*xmm_crc0, xmm_fold4, 0x01); + x_tmp0 = _mm_clmulepi64_si128(x_tmp0, xmm_fold4, 0x10); + ps_crc0 = _mm_castsi128_ps(*xmm_crc0); + ps_t0 = _mm_castsi128_ps(x_tmp0); + ps_res0 = _mm_xor_ps(ps_crc0, ps_t0); + + *xmm_crc1 = _mm_clmulepi64_si128(*xmm_crc1, xmm_fold4, 0x01); + x_tmp1 = _mm_clmulepi64_si128(x_tmp1, xmm_fold4, 0x10); + ps_crc1 = _mm_castsi128_ps(*xmm_crc1); + ps_t1 = _mm_castsi128_ps(x_tmp1); + ps_res1 = _mm_xor_ps(ps_crc1, ps_t1); + + *xmm_crc2 = _mm_clmulepi64_si128(*xmm_crc2, xmm_fold4, 0x01); + x_tmp2 = _mm_clmulepi64_si128(x_tmp2, xmm_fold4, 0x10); + ps_crc2 = _mm_castsi128_ps(*xmm_crc2); + ps_t2 = _mm_castsi128_ps(x_tmp2); + ps_res2 = _mm_xor_ps(ps_crc2, ps_t2); + + *xmm_crc3 = _mm_clmulepi64_si128(*xmm_crc3, xmm_fold4, 0x01); + x_tmp3 = _mm_clmulepi64_si128(x_tmp3, xmm_fold4, 0x10); + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + ps_t3 = _mm_castsi128_ps(x_tmp3); + ps_res3 = _mm_xor_ps(ps_crc3, ps_t3); + + *xmm_crc0 = _mm_castps_si128(ps_res0); + *xmm_crc1 = _mm_castps_si128(ps_res1); + *xmm_crc2 = _mm_castps_si128(ps_res2); + *xmm_crc3 = _mm_castps_si128(ps_res3); +} + +local const unsigned zalign(32) pshufb_shf_table[60] = { + 0x84838281,0x88878685,0x8c8b8a89,0x008f8e8d, /* shl 15 (16 - 1)/shr1 */ + 0x85848382,0x89888786,0x8d8c8b8a,0x01008f8e, /* shl 14 (16 - 3)/shr2 */ + 0x86858483,0x8a898887,0x8e8d8c8b,0x0201008f, /* shl 13 (16 - 4)/shr3 */ + 0x87868584,0x8b8a8988,0x8f8e8d8c,0x03020100, /* shl 12 (16 - 4)/shr4 */ + 0x88878685,0x8c8b8a89,0x008f8e8d,0x04030201, /* shl 11 (16 - 5)/shr5 */ + 0x89888786,0x8d8c8b8a,0x01008f8e,0x05040302, /* shl 10 (16 - 6)/shr6 */ + 0x8a898887,0x8e8d8c8b,0x0201008f,0x06050403, /* shl 9 (16 - 7)/shr7 */ + 0x8b8a8988,0x8f8e8d8c,0x03020100,0x07060504, /* shl 8 (16 - 8)/shr8 */ + 0x8c8b8a89,0x008f8e8d,0x04030201,0x08070605, /* shl 7 (16 - 9)/shr9 */ + 0x8d8c8b8a,0x01008f8e,0x05040302,0x09080706, /* shl 6 (16 -10)/shr10*/ + 0x8e8d8c8b,0x0201008f,0x06050403,0x0a090807, /* shl 5 (16 -11)/shr11*/ + 0x8f8e8d8c,0x03020100,0x07060504,0x0b0a0908, /* shl 4 (16 -12)/shr12*/ + 0x008f8e8d,0x04030201,0x08070605,0x0c0b0a09, /* shl 3 (16 -13)/shr13*/ + 0x01008f8e,0x05040302,0x09080706,0x0d0c0b0a, /* shl 2 (16 -14)/shr14*/ + 0x0201008f,0x06050403,0x0a090807,0x0e0d0c0b /* shl 1 (16 -15)/shr15*/ +}; + +TARGET_SSE42_PCLMUL +local void partial_fold(deflate_state *const s, const size_t len, + __m128i *xmm_crc0, __m128i *xmm_crc1, + __m128i *xmm_crc2, __m128i *xmm_crc3, + __m128i *xmm_crc_part) +{ + + const __m128i xmm_fold4 = _mm_set_epi32( + 0x00000001, 0x54442bd4, + 0x00000001, 0xc6e41596); + const __m128i xmm_mask3 = _mm_set1_epi32(0x80808080); + + __m128i xmm_shl, xmm_shr, xmm_tmp1, xmm_tmp2, xmm_tmp3; + __m128i xmm_a0_0, xmm_a0_1; + __m128 ps_crc3, psa0_0, psa0_1, ps_res; + + xmm_shl = _mm_load_si128((__m128i *)pshufb_shf_table + (len - 1)); + xmm_shr = xmm_shl; + xmm_shr = _mm_xor_si128(xmm_shr, xmm_mask3); + + xmm_a0_0 = _mm_shuffle_epi8(*xmm_crc0, xmm_shl); + + *xmm_crc0 = _mm_shuffle_epi8(*xmm_crc0, xmm_shr); + xmm_tmp1 = _mm_shuffle_epi8(*xmm_crc1, xmm_shl); + *xmm_crc0 = _mm_or_si128(*xmm_crc0, xmm_tmp1); + + *xmm_crc1 = _mm_shuffle_epi8(*xmm_crc1, xmm_shr); + xmm_tmp2 = _mm_shuffle_epi8(*xmm_crc2, xmm_shl); + *xmm_crc1 = _mm_or_si128(*xmm_crc1, xmm_tmp2); + + *xmm_crc2 = _mm_shuffle_epi8(*xmm_crc2, xmm_shr); + xmm_tmp3 = _mm_shuffle_epi8(*xmm_crc3, xmm_shl); + *xmm_crc2 = _mm_or_si128(*xmm_crc2, xmm_tmp3); + + *xmm_crc3 = _mm_shuffle_epi8(*xmm_crc3, xmm_shr); + *xmm_crc_part = _mm_shuffle_epi8(*xmm_crc_part, xmm_shl); + *xmm_crc3 = _mm_or_si128(*xmm_crc3, *xmm_crc_part); + + xmm_a0_1 = _mm_clmulepi64_si128(xmm_a0_0, xmm_fold4, 0x10); + xmm_a0_0 = _mm_clmulepi64_si128(xmm_a0_0, xmm_fold4, 0x01); + + ps_crc3 = _mm_castsi128_ps(*xmm_crc3); + psa0_0 = _mm_castsi128_ps(xmm_a0_0); + psa0_1 = _mm_castsi128_ps(xmm_a0_1); + + ps_res = _mm_xor_ps(ps_crc3, psa0_0); + ps_res = _mm_xor_ps(ps_res, psa0_1); + + *xmm_crc3 = _mm_castps_si128(ps_res); +} + +TARGET_SSE42_PCLMUL +ZLIB_INTERNAL void crc_fold_copy(deflate_state *const s, + unsigned char *dst, const unsigned char *src, long len) +{ + unsigned long algn_diff; + __m128i xmm_t0, xmm_t1, xmm_t2, xmm_t3; + + CRC_LOAD(s) + + if (len < 16) { + if (len == 0) + return; + goto partial; + } + + algn_diff = (0 - (uintptr_t)src) & 0xF; + if (algn_diff) { + xmm_crc_part = _mm_loadu_si128((__m128i *)src); + _mm_storeu_si128((__m128i *)dst, xmm_crc_part); + + dst += algn_diff; + src += algn_diff; + len -= algn_diff; + + partial_fold(s, algn_diff, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3, + &xmm_crc_part); + } + + while ((len -= 64) >= 0) { + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + xmm_t2 = _mm_load_si128((__m128i *)src + 2); + xmm_t3 = _mm_load_si128((__m128i *)src + 3); + + fold_4(s, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + _mm_storeu_si128((__m128i *)dst, xmm_t0); + _mm_storeu_si128((__m128i *)dst + 1, xmm_t1); + _mm_storeu_si128((__m128i *)dst + 2, xmm_t2); + _mm_storeu_si128((__m128i *)dst + 3, xmm_t3); + + xmm_crc0 = _mm_xor_si128(xmm_crc0, xmm_t0); + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_t1); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t3); + + src += 64; + dst += 64; + } + + /* + * len = num bytes left - 64 + */ + if (len + 16 >= 0) { + len += 16; + + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + xmm_t2 = _mm_load_si128((__m128i *)src + 2); + + fold_3(s, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + _mm_storeu_si128((__m128i *)dst, xmm_t0); + _mm_storeu_si128((__m128i *)dst + 1, xmm_t1); + _mm_storeu_si128((__m128i *)dst + 2, xmm_t2); + + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_t0); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t1); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t2); + + if (len == 0) + goto done; + + dst += 48; + src += 48; + } else if (len + 32 >= 0) { + len += 32; + + xmm_t0 = _mm_load_si128((__m128i *)src); + xmm_t1 = _mm_load_si128((__m128i *)src + 1); + + fold_2(s, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + _mm_storeu_si128((__m128i *)dst, xmm_t0); + _mm_storeu_si128((__m128i *)dst + 1, xmm_t1); + + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_t0); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t1); + + if (len == 0) + goto done; + + dst += 32; + src += 32; + } else if (len + 48 >= 0) { + len += 48; + + xmm_t0 = _mm_load_si128((__m128i *)src); + + fold_1(s, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3); + + _mm_storeu_si128((__m128i *)dst, xmm_t0); + + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_t0); + + if (len == 0) + goto done; + + dst += 16; + src += 16; + } else { + len += 64; + if (len == 0) + goto done; + } + +partial: + +#if defined(_MSC_VER) + /* VS does not permit the use of _mm_set_epi64x in 32-bit builds */ + { + int32_t parts[4] = {0, 0, 0, 0}; + memcpy(&parts, src, len); + xmm_crc_part = _mm_set_epi32(parts[3], parts[2], parts[1], parts[0]); + } +#else + { + int64_t parts[2] = {0, 0}; + memcpy(&parts, src, len); + xmm_crc_part = _mm_set_epi64x(parts[1], parts[0]); + } +#endif + + zmemcpy(dst, src, len); /* TODO: Possibly generate more efficient code. */ + partial_fold(s, len, &xmm_crc0, &xmm_crc1, &xmm_crc2, &xmm_crc3, + &xmm_crc_part); +done: + CRC_SAVE(s) +} + +local const unsigned zalign(16) crc_k[] = { + 0xccaa009e, 0x00000000, /* rk1 */ + 0x751997d0, 0x00000001, /* rk2 */ + 0xccaa009e, 0x00000000, /* rk5 */ + 0x63cd6124, 0x00000001, /* rk6 */ + 0xf7011640, 0x00000001, /* rk7 */ + 0xdb710640, 0x00000001 /* rk8 */ +}; + +local const unsigned zalign(16) crc_mask[4] = { + 0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0x00000000 +}; + +local const unsigned zalign(16) crc_mask2[4] = { + 0x00000000, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF +}; + +TARGET_SSE42_PCLMUL +unsigned ZLIB_INTERNAL crc_fold_512to32(deflate_state *const s) +{ + const __m128i xmm_mask = _mm_load_si128((__m128i *)crc_mask); + const __m128i xmm_mask2 = _mm_load_si128((__m128i *)crc_mask2); + + unsigned crc; + __m128i x_tmp0, x_tmp1, x_tmp2, crc_fold; + + __m128i xmm_crc0 = _mm_loadu_si128((__m128i *)s->crc0 + 0); + __m128i xmm_crc1 = _mm_loadu_si128((__m128i *)s->crc0 + 1); + __m128i xmm_crc2 = _mm_loadu_si128((__m128i *)s->crc0 + 2); + __m128i xmm_crc3 = _mm_loadu_si128((__m128i *)s->crc0 + 3); + + /* + * k1 + */ + crc_fold = _mm_load_si128((__m128i *)crc_k); + + x_tmp0 = _mm_clmulepi64_si128(xmm_crc0, crc_fold, 0x10); + xmm_crc0 = _mm_clmulepi64_si128(xmm_crc0, crc_fold, 0x01); + xmm_crc1 = _mm_xor_si128(xmm_crc1, x_tmp0); + xmm_crc1 = _mm_xor_si128(xmm_crc1, xmm_crc0); + + x_tmp1 = _mm_clmulepi64_si128(xmm_crc1, crc_fold, 0x10); + xmm_crc1 = _mm_clmulepi64_si128(xmm_crc1, crc_fold, 0x01); + xmm_crc2 = _mm_xor_si128(xmm_crc2, x_tmp1); + xmm_crc2 = _mm_xor_si128(xmm_crc2, xmm_crc1); + + x_tmp2 = _mm_clmulepi64_si128(xmm_crc2, crc_fold, 0x10); + xmm_crc2 = _mm_clmulepi64_si128(xmm_crc2, crc_fold, 0x01); + xmm_crc3 = _mm_xor_si128(xmm_crc3, x_tmp2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + + /* + * k5 + */ + crc_fold = _mm_load_si128((__m128i *)crc_k + 1); + + xmm_crc0 = xmm_crc3; + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0); + xmm_crc0 = _mm_srli_si128(xmm_crc0, 8); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc0); + + xmm_crc0 = xmm_crc3; + xmm_crc3 = _mm_slli_si128(xmm_crc3, 4); + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0x10); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc0); + xmm_crc3 = _mm_and_si128(xmm_crc3, xmm_mask2); + + /* + * k7 + */ + xmm_crc1 = xmm_crc3; + xmm_crc2 = xmm_crc3; + crc_fold = _mm_load_si128((__m128i *)crc_k + 2); + + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + xmm_crc3 = _mm_and_si128(xmm_crc3, xmm_mask); + + xmm_crc2 = xmm_crc3; + xmm_crc3 = _mm_clmulepi64_si128(xmm_crc3, crc_fold, 0x10); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc2); + xmm_crc3 = _mm_xor_si128(xmm_crc3, xmm_crc1); + + crc = _mm_extract_epi32(xmm_crc3, 2); + return ~crc; +} + +#endif /* CRC32_SIMD_SSE42_PCLMUL */ diff --git a/packages/runtime/vendor/zlib/deflate.c b/packages/runtime/vendor/zlib/deflate.c index 012ea8148..b98a985e5 100644 --- a/packages/runtime/vendor/zlib/deflate.c +++ b/packages/runtime/vendor/zlib/deflate.c @@ -1,5 +1,5 @@ /* deflate.c -- compress data using the deflation algorithm - * Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler + * Copyright (C) 1995-2026 Jean-loup Gailly and Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -37,7 +37,7 @@ * REFERENCES * * Deutsch, L.P.,"DEFLATE Compressed Data Format Specification". - * Available in http://tools.ietf.org/html/rfc1951 + * Available at https://datatracker.ietf.org/doc/html/rfc1951 * * A description of the Rabin and Karp algorithm is given in the book * "Algorithms" by R. Sedgewick, Addison-Wesley, p252. @@ -48,11 +48,30 @@ */ /* @(#) $Id$ */ - +#include #include "deflate.h" +#include "cpu_features.h" + +#if defined(DEFLATE_SLIDE_HASH_SSE2) || defined(DEFLATE_SLIDE_HASH_NEON) +#include "slide_hash_simd.h" +#endif + +#include "compare256.h" + +#if defined(QAT_COMPRESSION_ENABLED) +#include "contrib/qat/deflate_qat.h" +#endif + +#include "contrib/optimizations/insert_string.h" + +#ifdef FASTEST +/* See http://crbug.com/1113596 */ +#error "FASTEST is not supported in Chromium's zlib." +#endif + const char deflate_copyright[] = - " deflate 1.3.1 Copyright 1995-2024 Jean-loup Gailly and Mark Adler "; + " deflate 1.3.2.1 Copyright 1995-2026 Jean-loup Gailly and Mark Adler "; /* If you use the zlib library in a product, an acknowledgment is welcome in the documentation of your product. If for some reason you cannot @@ -78,6 +97,11 @@ local block_state deflate_slow(deflate_state *s, int flush); local block_state deflate_rle(deflate_state *s, int flush); local block_state deflate_huff(deflate_state *s, int flush); +/* From crc32.c */ +extern void ZLIB_INTERNAL crc_reset(deflate_state *const s); +extern void ZLIB_INTERNAL crc_finalize(deflate_state *const s); +extern void ZLIB_INTERNAL copy_with_crc(z_streamp strm, Bytef *dst, long size); + /* =========================================================================== * Local data */ @@ -132,46 +156,17 @@ local const config configuration_table[10] = { /* rank Z_BLOCK between Z_NO_FLUSH and Z_PARTIAL_FLUSH */ #define RANK(f) (((f) * 2) - ((f) > 4 ? 9 : 0)) -/* =========================================================================== - * Update a hash value with the given input byte - * IN assertion: all calls to UPDATE_HASH are made with consecutive input - * characters, so that a running hash key can be computed from the previous - * key instead of complete recalculation each time. - */ -#define UPDATE_HASH(s,h,c) (h = (((h) << s->hash_shift) ^ (c)) & s->hash_mask) - - -/* =========================================================================== - * Insert string str in the dictionary and set match_head to the previous head - * of the hash chain (the most recent string with same hash key). Return - * the previous length of the hash chain. - * If this file is compiled with -DFASTEST, the compression level is forced - * to 1, and no hash chains are maintained. - * IN assertion: all calls to INSERT_STRING are made with consecutive input - * characters and the first MIN_MATCH bytes of str are valid (except for - * the last MIN_MATCH-1 bytes of the input file). - */ -#ifdef FASTEST -#define INSERT_STRING(s, str, match_head) \ - (UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH-1)]), \ - match_head = s->head[s->ins_h], \ - s->head[s->ins_h] = (Pos)(str)) -#else -#define INSERT_STRING(s, str, match_head) \ - (UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH-1)]), \ - match_head = s->prev[(str) & s->w_mask] = s->head[s->ins_h], \ - s->head[s->ins_h] = (Pos)(str)) -#endif - /* =========================================================================== * Initialize the hash table (avoiding 64K overflow for 16 bit systems). * prev[] will be initialized on the fly. + * TODO(cavalcantii): optimization opportunity, check comments on: + * https://chromium-review.googlesource.com/c/chromium/src/+/3561506/ */ #define CLEAR_HASH(s) \ do { \ s->head[s->hash_size - 1] = NIL; \ - zmemzero((Bytef *)s->head, \ - (unsigned)(s->hash_size - 1)*sizeof(*s->head)); \ + zmemzero(s->head, (unsigned)(s->hash_size - 1)*sizeof(*s->head)); \ + s->slid = 0; \ } while (0) /* =========================================================================== @@ -185,6 +180,12 @@ local const config configuration_table[10] = { # endif #endif local void slide_hash(deflate_state *s) { +#if defined(DEFLATE_SLIDE_HASH_SSE2) || defined(DEFLATE_SLIDE_HASH_NEON) + slide_hash_simd(s->head, s->prev, s->w_size, s->hash_size); + s->slid = 1; + return; +#endif + unsigned n, m; Posf *p; uInt wsize = s->w_size; @@ -195,8 +196,8 @@ local void slide_hash(deflate_state *s) { m = *--p; *p = (Pos)(m >= wsize ? m - wsize : NIL); } while (--n); - n = wsize; #ifndef FASTEST + n = wsize; p = &s->prev[n]; do { m = *--p; @@ -206,6 +207,7 @@ local void slide_hash(deflate_state *s) { */ } while (--n); #endif + s->slid = 1; } /* =========================================================================== @@ -223,15 +225,19 @@ local unsigned read_buf(z_streamp strm, Bytef *buf, unsigned size) { strm->avail_in -= len; - zmemcpy(buf, strm->next_in, len); - if (strm->state->wrap == 1) { - strm->adler = adler32(strm->adler, buf, len); - } + /* TODO(cavalcantii): verify if we can remove 'copy_with_crc', it is legacy + * of the Intel optimizations dating back to 2015. + */ #ifdef GZIP - else if (strm->state->wrap == 2) { - strm->adler = crc32(strm->adler, buf, len); - } + if (strm->state->wrap == 2) + copy_with_crc(strm, buf, len); + else #endif + { + zmemcpy(buf, strm->next_in, len); + if (strm->state->wrap == 1) + strm->adler = adler32(strm->adler, buf, len); + } strm->next_in += len; strm->total_in += len; @@ -259,7 +265,14 @@ local void fill_window(deflate_state *s) { more = (unsigned)(s->window_size -(ulg)s->lookahead -(ulg)s->strstart); /* Deal with !@#$% 64K limit: */ +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4127) +#endif if (sizeof(int) <= 2) { +#ifdef _MSC_VER +#pragma warning(pop) +#endif if (more == 0 && s->strstart == 0 && s->lookahead == 0) { more = wsize; @@ -303,6 +316,20 @@ local void fill_window(deflate_state *s) { n = read_buf(s->strm, s->window + s->strstart + s->lookahead, more); s->lookahead += n; + /* Initialize the hash value now that we have some input: */ + if (s->chromium_zlib_hash) { + /* chromium hash reads 4 bytes */ + if (s->lookahead + s->insert > MIN_MATCH) { + uInt str = s->strstart - s->insert; + while (s->insert) { + insert_string(s, str); + str++; + s->insert--; + if (s->lookahead + s->insert <= MIN_MATCH) + break; + } + } + } else /* Initialize the hash value now that we have some input: */ if (s->lookahead + s->insert >= MIN_MATCH) { uInt str = s->strstart - s->insert; @@ -375,6 +402,8 @@ int ZEXPORT deflateInit_(z_streamp strm, int level, const char *version, /* To do: ignore strm->next_in if we use it as window */ } +#define WINDOW_PADDING 8 + /* ========================================================================= */ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, int windowBits, int memLevel, int strategy, @@ -383,6 +412,15 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, int wrap = 1; static const char my_version[] = ZLIB_VERSION; + // Needed to activate optimized insert_string() that helps compression + // for all wrapper formats (e.g. RAW, ZLIB, GZIP). + // Feature detection is not triggered while using RAW mode (i.e. we never + // call crc32() with a NULL buffer). +#if defined(CRC32_ARMV8_CRC32) || defined(CRC32_SIMD_SSE42_PCLMUL) \ + || defined(RISCV_RVV) + cpu_check_features(); +#endif + if (version == Z_NULL || version[0] != my_version[0] || stream_size != sizeof(z_stream)) { return Z_VERSION_ERROR; @@ -431,6 +469,7 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, if (windowBits == 8) windowBits = 9; /* until 256-byte window bug fixed */ s = (deflate_state *) ZALLOC(strm, 1, sizeof(deflate_state)); if (s == Z_NULL) return Z_MEM_ERROR; + zmemzero(s, sizeof(deflate_state)); strm->state = (struct internal_state FAR *)s; s->strm = strm; s->status = INIT_STATE; /* to pass state test in deflateReset() */ @@ -441,12 +480,23 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, s->w_size = 1 << s->w_bits; s->w_mask = s->w_size - 1; - s->hash_bits = (uInt)memLevel + 7; + s->chromium_zlib_hash = 1; +#if defined(USE_ZLIB_RABIN_KARP_ROLLING_HASH) + s->chromium_zlib_hash = 0; +#endif + + s->hash_bits = memLevel + 7; + if (s->chromium_zlib_hash && s->hash_bits < 15) { + s->hash_bits = 15; + } + s->hash_size = 1 << s->hash_bits; s->hash_mask = s->hash_size - 1; s->hash_shift = ((s->hash_bits + MIN_MATCH-1) / MIN_MATCH); - s->window = (Bytef *) ZALLOC(strm, s->w_size, 2*sizeof(Byte)); + s->window = (Bytef *) ZALLOC(strm, + s->w_size + WINDOW_PADDING, + 2*sizeof(Byte)); s->prev = (Posf *) ZALLOC(strm, s->w_size, sizeof(Pos)); s->head = (Posf *) ZALLOC(strm, s->hash_size, sizeof(Pos)); @@ -492,8 +542,11 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, * the compressed data for a dynamic block also cannot overwrite the * symbols from which it is being constructed. */ - - s->pending_buf = (uchf *) ZALLOC(strm, s->lit_bufsize, LIT_BUFS); +#ifdef LIT_MEM + s->pending_buf = (uchf *) ZALLOC(strm, s->lit_bufsize, 5); +#else + s->pending_buf = (uchf *) ZALLOC(strm, s->lit_bufsize, 4); +#endif s->pending_buf_size = (ulg)s->lit_bufsize * 4; if (s->window == Z_NULL || s->prev == Z_NULL || s->head == Z_NULL || @@ -503,6 +556,13 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, deflateEnd (strm); return Z_MEM_ERROR; } + /* Avoid use of unitialized values in the window, see crbug.com/1137613 and + * crbug.com/1144420 */ + zmemzero(s->window, (s->w_size + WINDOW_PADDING) * (2 * sizeof(Byte))); + /* Avoid use of uninitialized value, see: + * https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=11360 + */ + zmemzero(s->prev, s->w_size * sizeof(Pos)); #ifdef LIT_MEM s->d_buf = (ushf *)(s->pending_buf + (s->lit_bufsize << 1)); s->l_buf = s->pending_buf + (s->lit_bufsize << 2); @@ -520,6 +580,13 @@ int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, s->strategy = strategy; s->method = (Byte)method; +#if defined(QAT_COMPRESSION_ENABLED) + s->qat_s = NULL; + if (s->level && qat_deflate_init() == Z_OK) { + s->qat_s = qat_deflate_state_init(s->level, s->wrap); + } +#endif + return deflateReset(strm); } @@ -589,11 +656,7 @@ int ZEXPORT deflateSetDictionary(z_streamp strm, const Bytef *dictionary, str = s->strstart; n = s->lookahead - (MIN_MATCH-1); do { - UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); -#ifndef FASTEST - s->prev[str & s->w_mask] = s->head[s->ins_h]; -#endif - s->head[s->ins_h] = (Pos)str; + insert_string(s, str); str++; } while (--n); s->strstart = str; @@ -712,10 +775,23 @@ int ZEXPORT deflateSetHeader(z_streamp strm, gz_headerp head) { /* ========================================================================= */ int ZEXPORT deflatePending(z_streamp strm, unsigned *pending, int *bits) { if (deflateStateCheck(strm)) return Z_STREAM_ERROR; - if (pending != Z_NULL) - *pending = strm->state->pending; if (bits != Z_NULL) *bits = strm->state->bi_valid; + if (pending != Z_NULL) { + *pending = (unsigned)strm->state->pending; + if (*pending != strm->state->pending) { + *pending = (unsigned)-1; + return Z_BUF_ERROR; + } + } + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateUsed(z_streamp strm, int *bits) { + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + if (bits != Z_NULL) + *bits = strm->state->bi_used; return Z_OK; } @@ -739,7 +815,11 @@ int ZEXPORT deflatePrime(z_streamp strm, int bits, int value) { put = Buf_size - s->bi_valid; if (put > bits) put = bits; +#if defined(DEFLATE_CHUNK_WRITE_64LE) + s->bi_buf |= (uint64_t)((value & ((1ULL << put) - 1)) << s->bi_valid); +#else s->bi_buf |= (ush)((value & ((1 << put) - 1)) << s->bi_valid); +#endif /* DEFLATE_CHUNK_WRITE_64LE */ s->bi_valid += put; _tr_flush_bits(s); value >>= put; @@ -831,28 +911,34 @@ int ZEXPORT deflateTune(z_streamp strm, int good_length, int max_lazy, * * Shifts are used to approximate divisions, for speed. */ -uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen) { +z_size_t ZEXPORT deflateBound_z(z_streamp strm, z_size_t sourceLen) { deflate_state *s; - uLong fixedlen, storelen, wraplen; + z_size_t fixedlen, storelen, wraplen, bound; /* upper bound for fixed blocks with 9-bit literals and length 255 (memLevel == 2, which is the lowest that may not use stored blocks) -- ~13% overhead plus a small constant */ fixedlen = sourceLen + (sourceLen >> 3) + (sourceLen >> 8) + (sourceLen >> 9) + 4; + if (fixedlen < sourceLen) + fixedlen = (z_size_t)-1; /* upper bound for stored blocks with length 127 (memLevel == 1) -- ~4% overhead plus a small constant */ storelen = sourceLen + (sourceLen >> 5) + (sourceLen >> 7) + (sourceLen >> 11) + 7; + if (storelen < sourceLen) + storelen = (z_size_t)-1; - /* if can't get parameters, return larger bound plus a zlib wrapper */ - if (deflateStateCheck(strm)) - return (fixedlen > storelen ? fixedlen : storelen) + 6; + /* if can't get parameters, return larger bound plus a wrapper */ + if (deflateStateCheck(strm)) { + bound = fixedlen > storelen ? fixedlen : storelen; + return bound + 18 < bound ? (z_size_t)-1 : bound + 18; + } /* compute wrapper length */ s = strm->state; - switch (s->wrap) { + switch (s->wrap < 0 ? -s->wrap : s->wrap) { case 0: /* raw deflate */ wraplen = 0; break; @@ -882,18 +968,33 @@ uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen) { break; #endif default: /* for compiler happiness */ - wraplen = 6; + wraplen = 18; + } + + /* With Chromium's hashing, s->hash_bits may not correspond to the + memLevel, making the computations below incorrect. Return the + conservative bound. */ + if (s->chromium_zlib_hash) { + bound = fixedlen > storelen ? fixedlen : storelen; + return bound + wraplen < bound ? (z_size_t)-1 : bound + wraplen; } /* if not default parameters, return one of the conservative bounds */ - if (s->w_bits != 15 || s->hash_bits != 8 + 7) - return (s->w_bits <= s->hash_bits && s->level ? fixedlen : storelen) + - wraplen; + if (s->w_bits != 15 || s->hash_bits != 8 + 7) { + bound = s->w_bits <= s->hash_bits && s->level ? fixedlen : + storelen; + return bound + wraplen < bound ? (z_size_t)-1 : bound + wraplen; + } /* default settings: return tight bound for that case -- ~0.03% overhead plus a small constant */ - return sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + - (sourceLen >> 25) + 13 - 6 + wraplen; + bound = sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + + (sourceLen >> 25) + 13 - 6 + wraplen; + return bound < sourceLen ? (z_size_t)-1 : bound; +} +uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen) { + z_size_t bound = deflateBound_z(strm, sourceLen); + return (uLong)bound != bound ? (uLong)-1 : (uLong)bound; } /* ========================================================================= @@ -916,9 +1017,15 @@ local void flush_pending(z_streamp strm) { unsigned len; deflate_state *s = strm->state; +#if defined(QAT_COMPRESSION_ENABLED) + if (s->qat_s) { + qat_flush_pending(s); + } +#endif + _tr_flush_bits(s); - len = s->pending; - if (len > strm->avail_out) len = strm->avail_out; + len = s->pending > strm->avail_out ? strm->avail_out : + (unsigned)s->pending; if (len == 0) return; zmemcpy(strm->next_out, s->pending_out, len); @@ -938,8 +1045,8 @@ local void flush_pending(z_streamp strm) { #define HCRC_UPDATE(beg) \ do { \ if (s->gzhead->hcrc && s->pending > (beg)) \ - strm->adler = crc32(strm->adler, s->pending_buf + (beg), \ - s->pending - (beg)); \ + strm->adler = crc32_z(strm->adler, s->pending_buf + (beg), \ + s->pending - (beg)); \ } while (0) /* ========================================================================= */ @@ -1030,7 +1137,7 @@ int ZEXPORT deflate(z_streamp strm, int flush) { #ifdef GZIP if (s->status == GZIP_STATE) { /* gzip header */ - strm->adler = crc32(0L, Z_NULL, 0); + crc_reset(s); put_byte(s, 31); put_byte(s, 139); put_byte(s, 8); @@ -1073,8 +1180,8 @@ int ZEXPORT deflate(z_streamp strm, int flush) { put_byte(s, (s->gzhead->extra_len >> 8) & 0xff); } if (s->gzhead->hcrc) - strm->adler = crc32(strm->adler, s->pending_buf, - s->pending); + strm->adler = crc32_z(strm->adler, s->pending_buf, + s->pending); s->gzindex = 0; s->status = EXTRA_STATE; } @@ -1082,9 +1189,9 @@ int ZEXPORT deflate(z_streamp strm, int flush) { if (s->status == EXTRA_STATE) { if (s->gzhead->extra != Z_NULL) { ulg beg = s->pending; /* start of bytes to update crc */ - uInt left = (s->gzhead->extra_len & 0xffff) - s->gzindex; + ulg left = (s->gzhead->extra_len & 0xffff) - s->gzindex; while (s->pending + left > s->pending_buf_size) { - uInt copy = s->pending_buf_size - s->pending; + ulg copy = s->pending_buf_size - s->pending; zmemcpy(s->pending_buf + s->pending, s->gzhead->extra + s->gzindex, copy); s->pending = s->pending_buf_size; @@ -1231,6 +1338,7 @@ int ZEXPORT deflate(z_streamp strm, int flush) { /* Write the trailer */ #ifdef GZIP if (s->wrap == 2) { + crc_finalize(s); put_byte(s, (Byte)(strm->adler & 0xff)); put_byte(s, (Byte)((strm->adler >> 8) & 0xff)); put_byte(s, (Byte)((strm->adler >> 16) & 0xff)); @@ -1268,6 +1376,12 @@ int ZEXPORT deflateEnd(z_streamp strm) { TRY_FREE(strm, strm->state->prev); TRY_FREE(strm, strm->state->window); +#if defined(QAT_COMPRESSION_ENABLED) + if (strm->state->qat_s) { + qat_deflate_state_free(strm->state); + } +#endif + ZFREE(strm, strm->state); strm->state = Z_NULL; @@ -1295,18 +1409,25 @@ int ZEXPORT deflateCopy(z_streamp dest, z_streamp source) { ss = source->state; - zmemcpy((voidpf)dest, (voidpf)source, sizeof(z_stream)); + zmemcpy(dest, source, sizeof(z_stream)); ds = (deflate_state *) ZALLOC(dest, 1, sizeof(deflate_state)); if (ds == Z_NULL) return Z_MEM_ERROR; + zmemzero(ds, sizeof(deflate_state)); dest->state = (struct internal_state FAR *) ds; - zmemcpy((voidpf)ds, (voidpf)ss, sizeof(deflate_state)); + zmemcpy(ds, ss, sizeof(deflate_state)); ds->strm = dest; - ds->window = (Bytef *) ZALLOC(dest, ds->w_size, 2*sizeof(Byte)); + ds->window = (Bytef *) ZALLOC(dest, + ds->w_size + WINDOW_PADDING, + 2*sizeof(Byte)); ds->prev = (Posf *) ZALLOC(dest, ds->w_size, sizeof(Pos)); ds->head = (Posf *) ZALLOC(dest, ds->hash_size, sizeof(Pos)); - ds->pending_buf = (uchf *) ZALLOC(dest, ds->lit_bufsize, LIT_BUFS); +#ifdef LIT_MEM + ds->pending_buf = (uchf *) ZALLOC(dest, ds->lit_bufsize, 5); +#else + ds->pending_buf = (uchf *) ZALLOC(dest, ds->lit_bufsize, 4); +#endif if (ds->window == Z_NULL || ds->prev == Z_NULL || ds->head == Z_NULL || ds->pending_buf == Z_NULL) { @@ -1314,23 +1435,36 @@ int ZEXPORT deflateCopy(z_streamp dest, z_streamp source) { return Z_MEM_ERROR; } /* following zmemcpy do not work for 16-bit MSDOS */ - zmemcpy(ds->window, ss->window, ds->w_size * 2 * sizeof(Byte)); - zmemcpy((voidpf)ds->prev, (voidpf)ss->prev, ds->w_size * sizeof(Pos)); - zmemcpy((voidpf)ds->head, (voidpf)ss->head, ds->hash_size * sizeof(Pos)); - zmemcpy(ds->pending_buf, ss->pending_buf, ds->lit_bufsize * LIT_BUFS); + zmemcpy(ds->window, ss->window, ss->high_water); + zmemcpy(ds->prev, ss->prev, + (ss->slid || ss->strstart - ss->insert > ds->w_size ? ds->w_size : + ss->strstart - ss->insert) * sizeof(Pos)); + zmemcpy(ds->head, ss->head, ds->hash_size * sizeof(Pos)); ds->pending_out = ds->pending_buf + (ss->pending_out - ss->pending_buf); + zmemcpy(ds->pending_out, ss->pending_out, ss->pending); #ifdef LIT_MEM ds->d_buf = (ushf *)(ds->pending_buf + (ds->lit_bufsize << 1)); ds->l_buf = ds->pending_buf + (ds->lit_bufsize << 2); + zmemcpy(ds->d_buf, ss->d_buf, ss->sym_next * sizeof(ush)); + zmemcpy(ds->l_buf, ss->l_buf, ss->sym_next); #else ds->sym_buf = ds->pending_buf + ds->lit_bufsize; + zmemcpy(ds->sym_buf, ss->sym_buf, ss->sym_next); #endif ds->l_desc.dyn_tree = ds->dyn_ltree; ds->d_desc.dyn_tree = ds->dyn_dtree; ds->bl_desc.dyn_tree = ds->bl_tree; +#if defined(QAT_COMPRESSION_ENABLED) + if(ss->qat_s) { + ds->qat_s = qat_deflate_copy(ss); + if (!ds->qat_s) + return Z_MEM_ERROR; + } +#endif + return Z_OK; #endif /* MAXSEG_64K */ } @@ -1347,9 +1481,9 @@ int ZEXPORT deflateCopy(z_streamp dest, z_streamp source) { */ local uInt longest_match(deflate_state *s, IPos cur_match) { unsigned chain_length = s->max_chain_length;/* max hash chain length */ - register Bytef *scan = s->window + s->strstart; /* current string */ - register Bytef *match; /* matched string */ - register int len; /* length of current match */ + Bytef *scan = s->window + s->strstart; /* current string */ + Bytef *match; /* matched string */ + int len; /* length of current match */ int best_len = (int)s->prev_length; /* best match length so far */ int nice_match = s->nice_match; /* stop if match long enough */ IPos limit = s->strstart > (IPos)MAX_DIST(s) ? @@ -1360,17 +1494,23 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { Posf *prev = s->prev; uInt wmask = s->w_mask; -#ifdef UNALIGNED_OK +#if defined(DEFLATE_COMPARE256_64LE) + /* Quickly reject non-matches with the 2-byte prefilter + * (scan_start/scan_end); candidates that pass are extended 8 bytes at a + * time with compare256(). */ + uInt scan_start = read16(scan); + uInt scan_end = read16(scan + best_len - 1); +#elif defined(UNALIGNED_OK) /* Compare two bytes at a time. Note: this is not always beneficial. * Try with and without -DUNALIGNED_OK to check. */ - register Bytef *strend = s->window + s->strstart + MAX_MATCH - 1; - register ush scan_start = *(ushf*)scan; - register ush scan_end = *(ushf*)(scan + best_len - 1); + Bytef *strend = s->window + s->strstart + MAX_MATCH - 1; + ush scan_start = *(ushf*)scan; + ush scan_end = *(ushf*)(scan + best_len - 1); #else - register Bytef *strend = s->window + s->strstart + MAX_MATCH; - register Byte scan_end1 = scan[best_len - 1]; - register Byte scan_end = scan[best_len]; + Bytef *strend = s->window + s->strstart + MAX_MATCH; + Byte scan_end1 = scan[best_len - 1]; + Byte scan_end = scan[best_len]; #endif /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. @@ -1402,7 +1542,13 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { * However the length of the match is limited to the lookahead, so * the output of deflate is not affected by the uninitialized values. */ -#if (defined(UNALIGNED_OK) && MAX_MATCH == 258) +#if defined(DEFLATE_COMPARE256_64LE) + if (read16(match + best_len - 1) != scan_end || + read16(match) != scan_start) continue; + /* scan_start matched, so bytes 0..1 are equal; compare the remaining + * up to 256 bytes so the length can still reach MAX_MATCH (258). */ + len = 2 + compare256(scan + 2, match + 2); +#elif (defined(UNALIGNED_OK) && MAX_MATCH == 258) /* This code assumes sizeof(unsigned short) == 2. Do not use * UNALIGNED_OK if your compiler uses a different size. */ @@ -1418,7 +1564,16 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { * necessary to put more guard bytes at the end of the window, or * to check more often for insufficient lookahead. */ - Assert(scan[2] == match[2], "scan[2]?"); + if (!s->chromium_zlib_hash) { + Assert(scan[2] == match[2], "scan[2]?"); + } else { + /* When using CRC hashing, scan[2] and match[2] may mismatch, but in + * that case at least one of the other hashed bytes will mismatch + * also. Bytes 0 and 1 were already checked above, and we know there + * are at least four bytes to check otherwise the mismatch would have + * been found by the scan_end comparison above, so: */ + Assert(scan[2] == match[2] || scan[3] != match[3], "scan[2]??"); + } scan++, match++; do { } while (*(ushf*)(scan += 2) == *(ushf*)(match += 2) && @@ -1429,7 +1584,7 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { /* The funny "do {}" generates better code on most compilers */ /* Here, scan <= window + strstart + 257 */ - Assert(scan <= s->window + (unsigned)(s->window_size - 1), + Assert(scan <= s->window+(unsigned)(s->window_size - 1), "wild scan"); if (*scan == *match) scan++; @@ -1438,10 +1593,10 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { #else /* UNALIGNED_OK */ - if (match[best_len] != scan_end || + if (match[best_len] != scan_end || match[best_len - 1] != scan_end1 || - *match != *scan || - *++match != scan[1]) continue; + *match != *scan || + *++match != scan[1]) continue; /* The check at best_len - 1 can be removed because it will be made * again later. (This heuristic is not always a win.) @@ -1450,7 +1605,16 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { * the hash keys are equal and that HASH_BITS >= 8. */ scan += 2, match++; - Assert(*scan == *match, "match[2]?"); + if (!s->chromium_zlib_hash) { + Assert(*scan == *match, "match[2]?"); + } else { + /* When using CRC hashing, scan[2] and match[2] may mismatch, but in + * that case at least one of the other hashed bytes will mismatch + * also. Bytes 0 and 1 were already checked above, and we know there + * are at least four bytes to check otherwise the mismatch would have + * been found by the scan_end comparison above, so: */ + Assert(*scan == *match || scan[1] != match[1], "match[2]??"); + } /* We check for insufficient lookahead only every 8th comparison; * the 256th check will be made at strstart + 258. @@ -1474,7 +1638,9 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { s->match_start = cur_match; best_len = len; if (len >= nice_match) break; -#ifdef UNALIGNED_OK +#if defined(DEFLATE_COMPARE256_64LE) + scan_end = read16(scan + best_len - 1); +#elif defined(UNALIGNED_OK) scan_end = *(ushf*)(scan + best_len - 1); #else scan_end1 = scan[best_len - 1]; @@ -1494,10 +1660,10 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { * Optimized version for FASTEST only */ local uInt longest_match(deflate_state *s, IPos cur_match) { - register Bytef *scan = s->window + s->strstart; /* current string */ - register Bytef *match; /* matched string */ - register int len; /* length of current match */ - register Bytef *strend = s->window + s->strstart + MAX_MATCH; + Bytef *scan = s->window + s->strstart; /* current string */ + Bytef *match; /* matched string */ + int len; /* length of current match */ + Bytef *strend = s->window + s->strstart + MAX_MATCH; /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. * It is easy to get rid of this optimization if necessary. @@ -1557,7 +1723,7 @@ local uInt longest_match(deflate_state *s, IPos cur_match) { local void check_match(deflate_state *s, IPos start, IPos match, int length) { /* check that the match is indeed a match */ Bytef *back = s->window + (int)match, *here = s->window + start; - IPos len = length; + IPos len = (IPos)length; if (match == (IPos)-1) { /* match starts one byte before the current window -- just compare the subsequent length-1 bytes */ @@ -1629,13 +1795,14 @@ local block_state deflate_stored(deflate_state *s, int flush) { * this is 32K. This can be as small as 507 bytes for memLevel == 1. For * large input and output buffers, the stored block size will be larger. */ - unsigned min_block = MIN(s->pending_buf_size - 5, s->w_size); + unsigned min_block = (unsigned)(MIN(s->pending_buf_size - 5, s->w_size)); /* Copy as many min_block or larger stored blocks directly to next_out as * possible. If flushing, copy the remaining available input to next_out as * stored blocks, if there is enough space. */ - unsigned len, left, have, last = 0; + int last = 0; + unsigned len, left, have; unsigned used = s->strm->avail_in; do { /* Set len to the maximum size block that we can copy directly with the @@ -1643,12 +1810,12 @@ local block_state deflate_stored(deflate_state *s, int flush) { * would be copied from what's left in the window. */ len = MAX_STORED; /* maximum deflate stored block length */ - have = (s->bi_valid + 42) >> 3; /* number of header bytes */ + have = ((unsigned)s->bi_valid + 42) >> 3; /* bytes in header */ if (s->strm->avail_out < have) /* need room for header */ break; /* maximum stored block length that will fit in avail_out: */ have = s->strm->avail_out - have; - left = s->strstart - s->block_start; /* bytes left in window */ + left = (unsigned)(s->strstart - s->block_start); /* window bytes */ if (len > (ulg)left + s->strm->avail_in) len = left + s->strm->avail_in; /* limit len to the input */ if (len > have) @@ -1671,10 +1838,10 @@ local block_state deflate_stored(deflate_state *s, int flush) { _tr_stored_block(s, (char *)0, 0L, last); /* Replace the lengths in the dummy stored block with len. */ - s->pending_buf[s->pending - 4] = len; - s->pending_buf[s->pending - 3] = len >> 8; - s->pending_buf[s->pending - 2] = ~len; - s->pending_buf[s->pending - 1] = ~len >> 8; + s->pending_buf[s->pending - 4] = (Bytef)len; + s->pending_buf[s->pending - 3] = (Bytef)(len >> 8); + s->pending_buf[s->pending - 2] = (Bytef)~len; + s->pending_buf[s->pending - 1] = (Bytef)(~len >> 8); /* Write the stored block header bytes. */ flush_pending(s->strm); @@ -1745,8 +1912,10 @@ local block_state deflate_stored(deflate_state *s, int flush) { s->high_water = s->strstart; /* If the last block was written to next_out, then done. */ - if (last) + if (last) { + s->bi_used = 8; return finish_done; + } /* If flushing and all input has been consumed, then done. */ if (flush != Z_NO_FLUSH && flush != Z_FINISH && @@ -1754,7 +1923,7 @@ local block_state deflate_stored(deflate_state *s, int flush) { return block_done; /* Fill the window with any remaining input. */ - have = s->window_size - s->strstart; + have = (unsigned)(s->window_size - s->strstart); if (s->strm->avail_in > have && s->block_start >= (long)s->w_size) { /* Slide the window down. */ s->block_start -= s->w_size; @@ -1781,11 +1950,11 @@ local block_state deflate_stored(deflate_state *s, int flush) { * have enough input for a worthy block, or if flushing and there is enough * room for the remaining input as a stored block in the pending buffer. */ - have = (s->bi_valid + 42) >> 3; /* number of header bytes */ + have = ((unsigned)s->bi_valid + 42) >> 3; /* bytes in header */ /* maximum stored block length that will fit in pending: */ - have = MIN(s->pending_buf_size - have, MAX_STORED); + have = (unsigned)MIN(s->pending_buf_size - have, MAX_STORED); min_block = MIN(have, s->w_size); - left = s->strstart - s->block_start; + left = (unsigned)(s->strstart - s->block_start); if (left >= min_block || ((left || flush == Z_FINISH) && flush != Z_NO_FLUSH && s->strm->avail_in == 0 && left <= have)) { @@ -1798,6 +1967,8 @@ local block_state deflate_stored(deflate_state *s, int flush) { } /* We've done all we can with the available input and output. */ + if (last) + s->bi_used = 8; return last ? finish_started : need_more; } @@ -1812,6 +1983,24 @@ local block_state deflate_fast(deflate_state *s, int flush) { IPos hash_head; /* head of the hash chain */ int bflush; /* set if current block must be flushed */ +#if defined(QAT_COMPRESSION_ENABLED) + if (s->qat_s) { + qat_block_state qat_block = qat_deflate_step(s, flush); + switch (qat_block) { + case qat_block_need_more: + return need_more; + case qat_block_done: + return block_done; + case qat_block_finish_started: + return finish_started; + case qat_block_finish_done: + return finish_done; + case qat_failure: + break; + } + } +#endif + for (;;) { /* Make sure that we always have enough lookahead, except * at the end of the input file. We need MAX_MATCH bytes @@ -1831,7 +2020,7 @@ local block_state deflate_fast(deflate_state *s, int flush) { */ hash_head = NIL; if (s->lookahead >= MIN_MATCH) { - INSERT_STRING(s, s->strstart, hash_head); + hash_head = insert_string(s, s->strstart); } /* Find the longest match, discarding those <= prev_length. @@ -1846,7 +2035,7 @@ local block_state deflate_fast(deflate_state *s, int flush) { /* longest_match() sets match_start */ } if (s->match_length >= MIN_MATCH) { - check_match(s, s->strstart, s->match_start, s->match_length); + check_match(s, s->strstart, s->match_start, (int)s->match_length); _tr_tally_dist(s, s->strstart - s->match_start, s->match_length - MIN_MATCH, bflush); @@ -1862,7 +2051,7 @@ local block_state deflate_fast(deflate_state *s, int flush) { s->match_length--; /* string at strstart already in table */ do { s->strstart++; - INSERT_STRING(s, s->strstart, hash_head); + hash_head = insert_string(s, s->strstart); /* strstart never exceeds WSIZE-MAX_MATCH, so there are * always MIN_MATCH bytes ahead. */ @@ -1873,14 +2062,17 @@ local block_state deflate_fast(deflate_state *s, int flush) { { s->strstart += s->match_length; s->match_length = 0; - s->ins_h = s->window[s->strstart]; - UPDATE_HASH(s, s->ins_h, s->window[s->strstart + 1]); + + if (!s->chromium_zlib_hash) { + s->ins_h = s->window[s->strstart]; + UPDATE_HASH(s, s->ins_h, s->window[s->strstart + 1]); #if MIN_MATCH != 3 - Call UPDATE_HASH() MIN_MATCH-3 more times + Call UPDATE_HASH() MIN_MATCH-3 more times #endif - /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not - * matter since it will be recomputed at next deflate call. - */ + /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not + * matter since it will be recomputed at next deflate call. + */ + } } } else { /* No match, output a literal byte */ @@ -1911,6 +2103,24 @@ local block_state deflate_slow(deflate_state *s, int flush) { IPos hash_head; /* head of hash chain */ int bflush; /* set if current block must be flushed */ +#if defined(QAT_COMPRESSION_ENABLED) + if (s->qat_s) { + qat_block_state qat_block = qat_deflate_step(s, flush); + switch (qat_block) { + case qat_block_need_more: + return need_more; + case qat_block_done: + return block_done; + case qat_block_finish_started: + return finish_started; + case qat_block_finish_done: + return finish_done; + case qat_failure: + break; + } + } +#endif + /* Process the input block. */ for (;;) { /* Make sure that we always have enough lookahead, except @@ -1931,7 +2141,7 @@ local block_state deflate_slow(deflate_state *s, int flush) { */ hash_head = NIL; if (s->lookahead >= MIN_MATCH) { - INSERT_STRING(s, s->strstart, hash_head); + hash_head = insert_string(s, s->strstart); } /* Find the longest match, discarding those <= prev_length. @@ -1968,7 +2178,7 @@ local block_state deflate_slow(deflate_state *s, int flush) { uInt max_insert = s->strstart + s->lookahead - MIN_MATCH; /* Do not insert strings in hash table beyond this. */ - check_match(s, s->strstart - 1, s->prev_match, s->prev_length); + check_match(s, s->strstart - 1, s->prev_match, (int)s->prev_length); _tr_tally_dist(s, s->strstart - 1 - s->prev_match, s->prev_length - MIN_MATCH, bflush); @@ -1982,7 +2192,7 @@ local block_state deflate_slow(deflate_state *s, int flush) { s->prev_length -= 2; do { if (++s->strstart <= max_insert) { - INSERT_STRING(s, s->strstart, hash_head); + hash_head = insert_string(s, s->strstart); } } while (--s->prev_length != 0); s->match_available = 0; @@ -2076,7 +2286,7 @@ local block_state deflate_rle(deflate_state *s, int flush) { /* Emit match if have run of MIN_MATCH or longer, else emit literal */ if (s->match_length >= MIN_MATCH) { - check_match(s, s->strstart, s->strstart - 1, s->match_length); + check_match(s, s->strstart, s->strstart - 1, (int)s->match_length); _tr_tally_dist(s, 1, s->match_length - MIN_MATCH, bflush); diff --git a/packages/runtime/vendor/zlib/deflate.h b/packages/runtime/vendor/zlib/deflate.h index 300c6ada6..c55fd8f6a 100644 --- a/packages/runtime/vendor/zlib/deflate.h +++ b/packages/runtime/vendor/zlib/deflate.h @@ -1,5 +1,5 @@ /* deflate.h -- internal compression state - * Copyright (C) 1995-2024 Jean-loup Gailly + * Copyright (C) 1995-2026 Jean-loup Gailly * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -13,6 +13,10 @@ #ifndef DEFLATE_H #define DEFLATE_H +#if defined(DEFLATE_CHUNK_WRITE_64LE) +#include +#endif + #include "zutil.h" /* define NO_GZIP when compiling if you want to disable gzip header and @@ -25,7 +29,7 @@ /* define LIT_MEM to slightly increase the speed of deflate (order 1% to 2%) at the cost of a larger memory footprint */ -/* #define LIT_MEM */ +#define LIT_MEM /* =========================================================================== * Internal compression state. @@ -52,7 +56,11 @@ #define MAX_BITS 15 /* All codes must not exceed MAX_BITS bits */ +#if defined(DEFLATE_CHUNK_WRITE_64LE) +#define Buf_size 64 +#else #define Buf_size 16 +#endif /* size of bit buffer in bi_buf */ #define INIT_STATE 42 /* zlib header -> BUSY_STATE */ @@ -113,8 +121,8 @@ typedef struct internal_state { ulg gzindex; /* where in extra, name, or comment */ Byte method; /* can only be DEFLATED */ int last_flush; /* value of flush param for previous deflate call */ - - /* used by deflate.c: */ + unsigned crc0[4 * 5]; + /* used by deflate.c: */ uInt w_size; /* LZ77 window size (32K by default) */ uInt w_bits; /* log2(w_size) (8..16) */ @@ -222,11 +230,9 @@ typedef struct internal_state { */ #ifdef LIT_MEM -# define LIT_BUFS 5 ushf *d_buf; /* buffer for distances */ uchf *l_buf; /* buffer for literals/lengths */ #else -# define LIT_BUFS 4 uchf *sym_buf; /* buffer for distances and literals/lengths */ #endif @@ -263,7 +269,11 @@ typedef struct internal_state { ulg bits_sent; /* bit length of compressed data sent mod 2^32 */ #endif +#if defined(DEFLATE_CHUNK_WRITE_64LE) + uint64_t bi_buf; +#else ush bi_buf; +#endif /* Output buffer. bits are inserted starting at the bottom (least * significant bits). */ @@ -271,6 +281,9 @@ typedef struct internal_state { /* Number of valid bits in bi_buf. All bits above the last valid bit * are always zero. */ + int bi_used; + /* Last number of used bits when going to a byte boundary. + */ ulg high_water; /* High water mark offset in window for initialized bytes -- bytes above @@ -279,6 +292,21 @@ typedef struct internal_state { * updated to the new high water mark. */ + uInt chromium_zlib_hash; + /* 0 if Rabin-Karp rolling hash is enabled, non-zero if chromium zlib + * hash is enabled. + */ + +#if defined(QAT_COMPRESSION_ENABLED) + /* Pointer to a struct that contains the current state of the QAT + * stream. + */ + struct qat_deflate *qat_s; +#endif + + int slid; + /* True if the hash table has been slid since it was cleared. */ + } FAR deflate_state; /* Output a byte on the stream. @@ -374,4 +402,12 @@ void ZLIB_INTERNAL _tr_stored_block(deflate_state *s, charf *buf, flush = _tr_tally(s, distance, length) #endif +/* Functions that are SIMD optimised on x86 */ +void ZLIB_INTERNAL crc_fold_init(deflate_state* const s); +void ZLIB_INTERNAL crc_fold_copy(deflate_state* const s, + unsigned char* dst, + const unsigned char* src, + long len); +unsigned ZLIB_INTERNAL crc_fold_512to32(deflate_state* const s); + #endif /* DEFLATE_H */ diff --git a/packages/runtime/vendor/zlib/gzguts.h b/packages/runtime/vendor/zlib/gzguts.h index eba72085b..f7946bdc5 100644 --- a/packages/runtime/vendor/zlib/gzguts.h +++ b/packages/runtime/vendor/zlib/gzguts.h @@ -1,5 +1,5 @@ /* gzguts.h -- zlib internal header definitions for gz* operations - * Copyright (C) 2004-2024 Mark Adler + * Copyright (C) 2004-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -17,6 +17,18 @@ # define ZLIB_INTERNAL #endif +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef _CRT_SECURE_NO_WARNINGS +# define _CRT_SECURE_NO_WARNINGS +# endif +# ifndef _CRT_NONSTDC_NO_DEPRECATE +# define _CRT_NONSTDC_NO_DEPRECATE +# endif +#endif + #include #include "zlib.h" #ifdef STDC @@ -25,8 +37,8 @@ # include #endif -#ifndef _POSIX_SOURCE -# define _POSIX_SOURCE +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 200112L #endif #include @@ -36,19 +48,13 @@ #if defined(__TURBOC__) || defined(_MSC_VER) || defined(_WIN32) # include +# include #endif -#if defined(_WIN32) +#if defined(_WIN32) && !defined(WIDECHAR) # define WIDECHAR #endif -#ifdef WINAPI_FAMILY -# define open _open -# define read _read -# define write _write -# define close _close -#endif - #ifdef NO_DEFLATE /* for compatibility with old definition */ # define NO_GZCOMPRESS #endif @@ -72,33 +78,28 @@ #endif #ifndef HAVE_VSNPRINTF -# ifdef MSDOS +# if !defined(NO_vsnprintf) && \ + (defined(MSDOS) || defined(__TURBOC__) || defined(__SASC) || \ + defined(VMS) || defined(__OS400) || defined(__MVS__)) /* vsnprintf may exist on some MS-DOS compilers (DJGPP?), but for now we just assume it doesn't. */ # define NO_vsnprintf # endif -# ifdef __TURBOC__ -# define NO_vsnprintf -# endif # ifdef WIN32 /* In Win32, vsnprintf is available as the "non-ANSI" _vsnprintf. */ -# if !defined(vsnprintf) && !defined(NO_vsnprintf) -# if !defined(_MSC_VER) || ( defined(_MSC_VER) && _MSC_VER < 1500 ) -# define vsnprintf _vsnprintf +# if !defined(_MSC_VER) || ( defined(_MSC_VER) && _MSC_VER < 1500 ) +# ifndef vsnprintf +# define vsnprintf _vsnprintf # endif # endif -# endif -# ifdef __SASC -# define NO_vsnprintf -# endif -# ifdef VMS -# define NO_vsnprintf -# endif -# ifdef __OS400__ -# define NO_vsnprintf -# endif -# ifdef __MVS__ -# define NO_vsnprintf +# elif !defined(__STDC_VERSION__) || __STDC_VERSION__-0 < 199901L +/* Otherwise if C89/90, assume no C99 snprintf() or vsnprintf() */ +# ifndef NO_snprintf +# define NO_snprintf +# endif +# ifndef NO_vsnprintf +# define NO_vsnprintf +# endif # endif #endif @@ -135,6 +136,11 @@ # endif #endif +/* some environments don't define EWOULDBLOCK */ +#ifndef EWOULDBLOCK +# define EWOULDBLOCK EAGAIN +#endif + /* provide prototypes for these when building zlib without LFS */ #if !defined(_LARGEFILE64_SOURCE) || _LFS64_LARGEFILE-0 == 0 ZEXTERN gzFile ZEXPORT gzopen64(const char *, const char *); @@ -182,7 +188,9 @@ typedef struct { unsigned char *out; /* output buffer (double-sized when reading) */ int direct; /* 0 if processing gzip, 1 if transparent */ /* just for reading */ + int junk; /* -1 = start, 1 = junk candidate, 0 = in gzip */ int how; /* 0: get header, 1: copy, 2: decompress */ + int again; /* true if EAGAIN or EWOULDBLOCK on last i/o */ z_off64_t start; /* where the gzip data started, for rewinding */ int eof; /* true if end of input file reached */ int past; /* true if read requested past end */ @@ -192,7 +200,6 @@ typedef struct { int reset; /* true if a reset is pending after a Z_FINISH */ /* seek request */ z_off64_t skip; /* amount to skip (already rewound if backwards) */ - int seek; /* true if seek request pending */ /* error information */ int err; /* error code */ char *msg; /* error message */ diff --git a/packages/runtime/vendor/zlib/gzlib.c b/packages/runtime/vendor/zlib/gzlib.c index 983153cc8..2db29c8f0 100644 --- a/packages/runtime/vendor/zlib/gzlib.c +++ b/packages/runtime/vendor/zlib/gzlib.c @@ -1,18 +1,21 @@ /* gzlib.c -- zlib functions common to reading and writing gzip files - * Copyright (C) 2004-2024 Mark Adler + * Copyright (C) 2004-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ #include "gzguts.h" -#if defined(_WIN32) && !defined(__BORLANDC__) +#if defined(__DJGPP__) +# define LSEEK llseek +#elif defined(_WIN32) && !defined(__BORLANDC__) && !defined(UNDER_CE) # define LSEEK _lseeki64 -#else -#if defined(_LARGEFILE64_SOURCE) && _LFS64_LARGEFILE-0 +# define OPEN open +#elif defined(_LARGEFILE64_SOURCE) && _LFS64_LARGEFILE-0 # define LSEEK lseek64 +# define OPEN open64 #else # define LSEEK lseek -#endif +# define OPEN open #endif #if defined UNDER_CE @@ -52,7 +55,7 @@ char ZLIB_INTERNAL *gz_strwinerror(DWORD error) { msgbuf[chars] = 0; } - wcstombs(buf, msgbuf, chars + 1); + wcstombs(buf, msgbuf, chars + 1); /* assumes buf is big enough */ LocalFree(msgbuf); } else { @@ -72,10 +75,12 @@ local void gz_reset(gz_statep state) { state->eof = 0; /* not at end of file */ state->past = 0; /* have not read past end yet */ state->how = LOOK; /* look for gzip header */ + state->junk = -1; /* mark first member */ } else /* for writing ... */ state->reset = 0; /* no deflateReset pending */ - state->seek = 0; /* no seek request pending */ + state->again = 0; /* no stalled i/o yet */ + state->skip = 0; /* no seek request pending */ gz_error(state, Z_OK, NULL); /* clear error */ state->x.pos = 0; /* no uncompressed data yet */ state->strm.avail_in = 0; /* no input data yet */ @@ -85,16 +90,13 @@ local void gz_reset(gz_statep state) { local gzFile gz_open(const void *path, int fd, const char *mode) { gz_statep state; z_size_t len; - int oflag; -#ifdef O_CLOEXEC - int cloexec = 0; -#endif + int oflag = 0; #ifdef O_EXCL int exclusive = 0; #endif /* check input */ - if (path == NULL) + if (path == NULL || mode == NULL) return NULL; /* allocate gzFile structure to return */ @@ -103,6 +105,7 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { return NULL; state->size = 0; /* no buffers allocated yet */ state->want = GZBUFSIZE; /* requested buffer size */ + state->err = Z_OK; /* no error yet */ state->msg = NULL; /* no error message yet */ /* interpret mode */ @@ -133,7 +136,7 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { break; #ifdef O_CLOEXEC case 'e': - cloexec = 1; + oflag |= O_CLOEXEC; break; #endif #ifdef O_EXCL @@ -153,6 +156,14 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { case 'F': state->strategy = Z_FIXED; break; + case 'G': + state->direct = -1; + break; +#ifdef O_NONBLOCK + case 'N': + oflag |= O_NONBLOCK; + break; +#endif case 'T': state->direct = 1; break; @@ -168,22 +179,30 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { return NULL; } - /* can't force transparent read */ + /* direct is 0, 1 if "T", or -1 if "G" (last "G" or "T" wins) */ if (state->mode == GZ_READ) { - if (state->direct) { + if (state->direct == 1) { + /* can't force a transparent read */ free(state); return NULL; } - state->direct = 1; /* for empty file */ + if (state->direct == 0) + /* default when reading is auto-detect of gzip vs. transparent -- + start with a transparent assumption in case of an empty file */ + state->direct = 1; } + else if (state->direct == -1) { + /* "G" has no meaning when writing -- disallow it */ + free(state); + return NULL; + } + /* if reading, direct == 1 for auto-detect, -1 for gzip only; if writing or + appending, direct == 0 for gzip, 1 for transparent (copy in to out) */ /* save the path name for error messages */ #ifdef WIDECHAR - if (fd == -2) { + if (fd == -2) len = wcstombs(NULL, path, 0); - if (len == (z_size_t)-1) - len = 0; - } else #endif len = strlen((const char *)path); @@ -193,29 +212,29 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { return NULL; } #ifdef WIDECHAR - if (fd == -2) + if (fd == -2) { if (len) wcstombs(state->path, path, len + 1); else *(state->path) = 0; + } else #endif + { #if !defined(NO_snprintf) && !defined(NO_vsnprintf) (void)snprintf(state->path, len + 1, "%s", (const char *)path); #else strcpy(state->path, path); #endif + } /* compute the flags for open() */ - oflag = + oflag |= #ifdef O_LARGEFILE O_LARGEFILE | #endif #ifdef O_BINARY O_BINARY | -#endif -#ifdef O_CLOEXEC - (cloexec ? O_CLOEXEC : 0) | #endif (state->mode == GZ_READ ? O_RDONLY : @@ -228,11 +247,23 @@ local gzFile gz_open(const void *path, int fd, const char *mode) { O_APPEND))); /* open the file with the appropriate flags (or just use fd) */ - state->fd = fd > -1 ? fd : ( + if (fd == -1) + state->fd = OPEN((const char *)path, oflag, 0666); #ifdef WIDECHAR - fd == -2 ? _wopen(path, oflag, 0666) : + else if (fd == -2) + state->fd = _wopen(path, oflag, _S_IREAD | _S_IWRITE); #endif - open((const char *)path, oflag, 0666)); + else { +#ifdef O_NONBLOCK + if (oflag & O_NONBLOCK) + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); +#endif +#ifdef O_CLOEXEC + if (oflag & O_CLOEXEC) + fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | O_CLOEXEC); +#endif + state->fd = fd; + } if (state->fd == -1) { free(state->path); free(state); @@ -359,9 +390,10 @@ z_off64_t ZEXPORT gzseek64(gzFile file, z_off64_t offset, int whence) { /* normalize offset to a SEEK_CUR specification */ if (whence == SEEK_SET) offset -= state->x.pos; - else if (state->seek) - offset += state->skip; - state->seek = 0; + else { + offset += state->past ? 0 : state->skip; + state->skip = 0; + } /* if within raw area while reading, just go there */ if (state->mode == GZ_READ && state->how == COPY && @@ -372,7 +404,7 @@ z_off64_t ZEXPORT gzseek64(gzFile file, z_off64_t offset, int whence) { state->x.have = 0; state->eof = 0; state->past = 0; - state->seek = 0; + state->skip = 0; gz_error(state, Z_OK, NULL); state->strm.avail_in = 0; state->x.pos += offset; @@ -401,10 +433,7 @@ z_off64_t ZEXPORT gzseek64(gzFile file, z_off64_t offset, int whence) { } /* request skip (if not zero) */ - if (offset) { - state->seek = 1; - state->skip = offset; - } + state->skip = offset; return state->x.pos + offset; } @@ -428,7 +457,7 @@ z_off64_t ZEXPORT gztell64(gzFile file) { return -1; /* return position */ - return state->x.pos + (state->seek ? state->skip : 0); + return state->x.pos + (state->past ? 0 : state->skip); } /* -- see zlib.h -- */ @@ -535,7 +564,7 @@ void ZLIB_INTERNAL gz_error(gz_statep state, int err, const char *msg) { } /* if fatal, set state->x.have to 0 so that the gzgetc() macro fails */ - if (err != Z_OK && err != Z_BUF_ERROR) + if (err != Z_OK && err != Z_BUF_ERROR && !state->again) state->x.have = 0; /* set error code, and if no message, then done */ @@ -572,6 +601,7 @@ unsigned ZLIB_INTERNAL gz_intmax(void) { return INT_MAX; #else unsigned p = 1, q; + do { q = p; p <<= 1; diff --git a/packages/runtime/vendor/zlib/gzread.c b/packages/runtime/vendor/zlib/gzread.c index 4168cbc88..bf41d63bc 100644 --- a/packages/runtime/vendor/zlib/gzread.c +++ b/packages/runtime/vendor/zlib/gzread.c @@ -1,5 +1,5 @@ /* gzread.c -- zlib functions for reading gzip files - * Copyright (C) 2004-2017 Mark Adler + * Copyright (C) 2004-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -8,23 +8,36 @@ /* Use read() to load a buffer -- return -1 on error, otherwise 0. Read from state->fd, and update state->eof, state->err, and state->msg as appropriate. This function needs to loop on read(), since read() is not guaranteed to - read the number of bytes requested, depending on the type of descriptor. */ + read the number of bytes requested, depending on the type of descriptor. It + also needs to loop to manage the fact that read() returns an int. If the + descriptor is non-blocking and read() returns with no data in order to avoid + blocking, then gz_load() will return 0 if some data has been read, or -1 if + no data has been read. Either way, state->again is set true to indicate a + non-blocking event. If errno is non-zero on return, then there was an error + signaled from read(). *have is set to the number of bytes read. */ local int gz_load(gz_statep state, unsigned char *buf, unsigned len, unsigned *have) { int ret; unsigned get, max = ((unsigned)-1 >> 2) + 1; + state->again = 0; + errno = 0; *have = 0; do { get = len - *have; if (get > max) get = max; - ret = read(state->fd, buf + *have, get); + ret = (int)read(state->fd, buf + *have, get); if (ret <= 0) break; *have += (unsigned)ret; } while (*have < len); if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + state->again = 1; + if (*have != 0) + return 0; + } gz_error(state, Z_ERRNO, zstrerror()); return -1; } @@ -50,10 +63,14 @@ local int gz_avail(gz_statep state) { if (strm->avail_in) { /* copy what's there to the start */ unsigned char *p = state->in; unsigned const char *q = strm->next_in; - unsigned n = strm->avail_in; - do { - *p++ = *q++; - } while (--n); + + if (q != p) { + unsigned n = strm->avail_in; + + do { + *p++ = *q++; + } while (--n); + } } if (gz_load(state, state->in + strm->avail_in, state->size - strm->avail_in, &got) == -1) @@ -104,39 +121,44 @@ local int gz_look(gz_statep state) { } } - /* get at least the magic bytes in the input buffer */ - if (strm->avail_in < 2) { - if (gz_avail(state) == -1) - return -1; - if (strm->avail_in == 0) - return 0; - } - - /* look for gzip magic bytes -- if there, do gzip decoding (note: there is - a logical dilemma here when considering the case of a partially written - gzip file, to wit, if a single 31 byte is written, then we cannot tell - whether this is a single-byte file, or just a partially written gzip - file -- for here we assume that if a gzip file is being written, then - the header will be written in a single operation, so that reading a - single byte is sufficient indication that it is not a gzip file) */ - if (strm->avail_in > 1 && - strm->next_in[0] == 31 && strm->next_in[1] == 139) { + /* if transparent reading is disabled, which would only be at the start, or + if we're looking for a gzip member after the first one, which is not at + the start, then proceed directly to look for a gzip member next */ + if (state->direct == -1 || state->junk == 0) { inflateReset(strm); state->how = GZIP; + state->junk = state->junk != -1; state->direct = 0; return 0; } - /* no gzip header -- if we were decoding gzip before, then this is trailing - garbage. Ignore the trailing garbage and finish. */ - if (state->direct == 0) { - strm->avail_in = 0; - state->eof = 1; - state->x.have = 0; + /* otherwise we're at the start with auto-detect -- we check to see if the + first four bytes could be gzip header in order to decide whether or not + this will be a transparent read */ + + /* load any header bytes into the input buffer -- if the input is empty, + then it's not an error as this is a transparent read of zero bytes */ + if (gz_avail(state) == -1) + return -1; + if (strm->avail_in == 0 || (state->again && strm->avail_in < 4)) + /* if non-blocking input stalled before getting four bytes, then + return and wait until a later call has accumulated enough */ + return 0; + + /* see if this is (likely) gzip input -- if the first four bytes are + consistent with a gzip header, then go look for the first gzip member, + otherwise proceed to copy the input transparently */ + if (strm->avail_in > 3 && + strm->next_in[0] == 31 && strm->next_in[1] == 139 && + strm->next_in[2] == 8 && strm->next_in[3] < 32) { + inflateReset(strm); + state->how = GZIP; + state->junk = 1; + state->direct = 0; return 0; } - /* doing raw i/o, copy any leftover input to output -- this assumes that + /* doing raw i/o: copy any leftover input to output -- this assumes that the output buffer is larger than the input buffer, which also assures space for gzungetc() */ state->x.next = state->out; @@ -144,15 +166,17 @@ local int gz_look(gz_statep state) { state->x.have = strm->avail_in; strm->avail_in = 0; state->how = COPY; - state->direct = 1; return 0; } /* Decompress from input to the provided next_out and avail_out in the state. On return, state->x.have and state->x.next point to the just decompressed - data. If the gzip stream completes, state->how is reset to LOOK to look for - the next gzip stream or raw data, once state->x.have is depleted. Returns 0 - on success, -1 on failure. */ + data. If the gzip stream completes, state->how is reset to LOOK to look for + the next gzip stream or raw data, once state->x.have is depleted. Returns 0 + on success, -1 on failure. If EOF is reached when looking for more input to + complete the gzip member, then an unexpected end of file error is raised. + If there is no more input, but state->again is true, then EOF has not been + reached, and no error is raised. */ local int gz_decomp(gz_statep state) { int ret = Z_OK; unsigned had; @@ -162,28 +186,41 @@ local int gz_decomp(gz_statep state) { had = strm->avail_out; do { /* get more input for inflate() */ - if (strm->avail_in == 0 && gz_avail(state) == -1) - return -1; + if (strm->avail_in == 0 && gz_avail(state) == -1) { + ret = state->err; + break; + } if (strm->avail_in == 0) { - gz_error(state, Z_BUF_ERROR, "unexpected end of file"); + if (!state->again) + gz_error(state, Z_BUF_ERROR, "unexpected end of file"); break; } /* decompress and handle errors */ ret = inflate(strm, Z_NO_FLUSH); + if (strm->avail_out < had) + /* any decompressed data marks this as a real gzip stream */ + state->junk = 0; if (ret == Z_STREAM_ERROR || ret == Z_NEED_DICT) { gz_error(state, Z_STREAM_ERROR, "internal error: inflate stream corrupt"); - return -1; + break; } if (ret == Z_MEM_ERROR) { gz_error(state, Z_MEM_ERROR, "out of memory"); - return -1; + break; } if (ret == Z_DATA_ERROR) { /* deflate stream invalid */ + if (state->junk == 1) { /* trailing garbage is ok */ + strm->avail_in = 0; + state->eof = 1; + state->how = LOOK; + ret = Z_OK; + break; + } gz_error(state, Z_DATA_ERROR, strm->msg == NULL ? "compressed data error" : strm->msg); - return -1; + break; } } while (strm->avail_out && ret != Z_STREAM_END); @@ -192,11 +229,14 @@ local int gz_decomp(gz_statep state) { state->x.next = strm->next_out - state->x.have; /* if the gzip stream completed successfully, look for another */ - if (ret == Z_STREAM_END) + if (ret == Z_STREAM_END) { + state->junk = 0; state->how = LOOK; + return 0; + } - /* good decompression */ - return 0; + /* return decompression status */ + return ret != Z_OK ? -1 : 0; } /* Fetch data and put it in the output buffer. Assumes state->x.have is 0. @@ -227,25 +267,31 @@ local int gz_fetch(gz_statep state) { strm->next_out = state->out; if (gz_decomp(state) == -1) return -1; + break; + default: + gz_error(state, Z_STREAM_ERROR, "state corrupt"); + return -1; } } while (state->x.have == 0 && (!state->eof || strm->avail_in)); return 0; } -/* Skip len uncompressed bytes of output. Return -1 on error, 0 on success. */ -local int gz_skip(gz_statep state, z_off64_t len) { +/* Skip state->skip (> 0) uncompressed bytes of output. Return -1 on error, 0 + on success. */ +local int gz_skip(gz_statep state) { unsigned n; /* skip over len bytes or reach end-of-file, whichever comes first */ - while (len) + do { /* skip over whatever is in output buffer */ if (state->x.have) { - n = GT_OFF(state->x.have) || (z_off64_t)state->x.have > len ? - (unsigned)len : state->x.have; + n = GT_OFF(state->x.have) || + (z_off64_t)state->x.have > state->skip ? + (unsigned)state->skip : state->x.have; state->x.have -= n; state->x.next += n; state->x.pos += n; - len -= n; + state->skip -= n; } /* output buffer empty -- return if we're at the end of the input */ @@ -258,30 +304,32 @@ local int gz_skip(gz_statep state, z_off64_t len) { if (gz_fetch(state) == -1) return -1; } + } while (state->skip); return 0; } /* Read len bytes into buf from file, or less than len up to the end of the - input. Return the number of bytes read. If zero is returned, either the - end of file was reached, or there was an error. state->err must be - consulted in that case to determine which. */ + input. Return the number of bytes read. If zero is returned, either the end + of file was reached, or there was an error. state->err must be consulted in + that case to determine which. If there was an error, but some uncompressed + bytes were read before the error, then that count is returned. The error is + still recorded, and so is deferred until the next call. */ local z_size_t gz_read(gz_statep state, voidp buf, z_size_t len) { z_size_t got; unsigned n; + int err; /* if len is zero, avoid unnecessary operations */ if (len == 0) return 0; /* process a skip request */ - if (state->seek) { - state->seek = 0; - if (gz_skip(state, state->skip) == -1) - return 0; - } + if (state->skip && gz_skip(state) == -1) + return 0; /* get len bytes to buf, or less than len if at the end */ got = 0; + err = 0; do { /* set n to the maximum amount of len that fits in an unsigned int */ n = (unsigned)-1; @@ -295,37 +343,36 @@ local z_size_t gz_read(gz_statep state, voidp buf, z_size_t len) { memcpy(buf, state->x.next, n); state->x.next += n; state->x.have -= n; + if (state->err != Z_OK) + /* caught deferred error from gz_fetch() */ + err = -1; } /* output buffer empty -- return if we're at the end of the input */ - else if (state->eof && state->strm.avail_in == 0) { - state->past = 1; /* tried to read past end */ + else if (state->eof && state->strm.avail_in == 0) break; - } /* need output data -- for small len or new stream load up our output - buffer */ + buffer, so that gzgetc() can be fast */ else if (state->how == LOOK || n < (state->size << 1)) { /* get more output, looking for header if required */ - if (gz_fetch(state) == -1) - return 0; + if (gz_fetch(state) == -1 && state->x.have == 0) + /* if state->x.have != 0, error will be caught after copy */ + err = -1; continue; /* no progress yet -- go back to copy above */ /* the copy above assures that we will leave with space in the output buffer, allowing at least one gzungetc() to succeed */ } /* large len -- read directly into user buffer */ - else if (state->how == COPY) { /* read directly */ - if (gz_load(state, (unsigned char *)buf, n, &n) == -1) - return 0; - } + else if (state->how == COPY) /* read directly */ + err = gz_load(state, (unsigned char *)buf, n, &n); /* large len -- decompress directly into user buffer */ else { /* state->how == GZIP */ state->strm.avail_out = n; state->strm.next_out = (unsigned char *)buf; - if (gz_decomp(state) == -1) - return 0; + err = gz_decomp(state); n = state->x.have; state->x.have = 0; } @@ -335,7 +382,11 @@ local z_size_t gz_read(gz_statep state, voidp buf, z_size_t len) { buf = (char *)buf + n; got += n; state->x.pos += n; - } while (len); + } while (len && !err); + + /* note read past eof */ + if (len && state->eof) + state->past = 1; /* return number of bytes read into user buffer */ return got; @@ -345,16 +396,18 @@ local z_size_t gz_read(gz_statep state, voidp buf, z_size_t len) { int ZEXPORT gzread(gzFile file, voidp buf, unsigned len) { gz_statep state; - /* get internal structure */ + /* get internal structure and check that it's for reading */ if (file == NULL) return -1; state = (gz_statep)file; - - /* check that we're reading and that there's no (serious) error */ - if (state->mode != GZ_READ || - (state->err != Z_OK && state->err != Z_BUF_ERROR)) + if (state->mode != GZ_READ) return -1; + /* check that there was no (serious) error */ + if (state->err != Z_OK && state->err != Z_BUF_ERROR && !state->again) + return -1; + gz_error(state, Z_OK, NULL); + /* since an int is returned, make sure len fits in one, otherwise return with an error (this avoids a flaw in the interface) */ if ((int)len < 0) { @@ -366,28 +419,40 @@ int ZEXPORT gzread(gzFile file, voidp buf, unsigned len) { len = (unsigned)gz_read(state, buf, len); /* check for an error */ - if (len == 0 && state->err != Z_OK && state->err != Z_BUF_ERROR) - return -1; + if (len == 0) { + if (state->err != Z_OK && state->err != Z_BUF_ERROR) + return -1; + if (state->again) { + /* non-blocking input stalled after some input was read, but no + uncompressed bytes were produced -- let the application know + this isn't EOF */ + gz_error(state, Z_ERRNO, zstrerror()); + return -1; + } + } - /* return the number of bytes read (this is assured to fit in an int) */ + /* return the number of bytes read */ return (int)len; } /* -- see zlib.h -- */ -z_size_t ZEXPORT gzfread(voidp buf, z_size_t size, z_size_t nitems, gzFile file) { +z_size_t ZEXPORT gzfread(voidp buf, z_size_t size, z_size_t nitems, + gzFile file) { z_size_t len; gz_statep state; - /* get internal structure */ + /* get internal structure and check that it's for reading */ if (file == NULL) return 0; state = (gz_statep)file; - - /* check that we're reading and that there's no (serious) error */ - if (state->mode != GZ_READ || - (state->err != Z_OK && state->err != Z_BUF_ERROR)) + if (state->mode != GZ_READ) return 0; + /* check that there was no (serious) error */ + if (state->err != Z_OK && state->err != Z_BUF_ERROR && !state->again) + return 0; + gz_error(state, Z_OK, NULL); + /* compute bytes to read -- error on overflow */ len = nitems * size; if (size && len / size != nitems) { @@ -404,21 +469,27 @@ z_size_t ZEXPORT gzfread(voidp buf, z_size_t size, z_size_t nitems, gzFile file) # undef z_gzgetc #else # undef gzgetc +# ifdef Z_CR_PREFIX_SET +# define gzgetc Cr_z_gzgetc +# endif #endif + int ZEXPORT gzgetc(gzFile file) { unsigned char buf[1]; gz_statep state; - /* get internal structure */ + /* get internal structure and check that it's for reading */ if (file == NULL) return -1; state = (gz_statep)file; - - /* check that we're reading and that there's no (serious) error */ - if (state->mode != GZ_READ || - (state->err != Z_OK && state->err != Z_BUF_ERROR)) + if (state->mode != GZ_READ) return -1; + /* check that there was no (serious) error */ + if (state->err != Z_OK && state->err != Z_BUF_ERROR && !state->again) + return -1; + gz_error(state, Z_OK, NULL); + /* try output buffer (no need to check for skip request) */ if (state->x.have) { state->x.have--; @@ -438,26 +509,25 @@ int ZEXPORT gzgetc_(gzFile file) { int ZEXPORT gzungetc(int c, gzFile file) { gz_statep state; - /* get internal structure */ + /* get internal structure and check that it's for reading */ if (file == NULL) return -1; state = (gz_statep)file; - - /* in case this was just opened, set up the input buffer */ - if (state->mode == GZ_READ && state->how == LOOK && state->x.have == 0) - (void)gz_look(state); - - /* check that we're reading and that there's no (serious) error */ - if (state->mode != GZ_READ || - (state->err != Z_OK && state->err != Z_BUF_ERROR)) + if (state->mode != GZ_READ) return -1; + /* in case this was just opened, set up the input buffer */ + if (state->how == LOOK && state->x.have == 0) + (void)gz_look(state); + + /* check that there was no (serious) error */ + if (state->err != Z_OK && state->err != Z_BUF_ERROR && !state->again) + return -1; + gz_error(state, Z_OK, NULL); + /* process a skip request */ - if (state->seek) { - state->seek = 0; - if (gz_skip(state, state->skip) == -1) - return -1; - } + if (state->skip && gz_skip(state) == -1) + return -1; /* can't push EOF */ if (c < 0) @@ -483,6 +553,7 @@ int ZEXPORT gzungetc(int c, gzFile file) { if (state->x.next == state->out) { unsigned char *src = state->out + state->x.have; unsigned char *dest = state->out + (state->size << 1); + while (src > state->out) *--dest = *--src; state->x.next = dest; @@ -502,32 +573,31 @@ char * ZEXPORT gzgets(gzFile file, char *buf, int len) { unsigned char *eol; gz_statep state; - /* check parameters and get internal structure */ + /* check parameters, get internal structure, and check that it's for + reading */ if (file == NULL || buf == NULL || len < 1) return NULL; state = (gz_statep)file; - - /* check that we're reading and that there's no (serious) error */ - if (state->mode != GZ_READ || - (state->err != Z_OK && state->err != Z_BUF_ERROR)) + if (state->mode != GZ_READ) return NULL; - /* process a skip request */ - if (state->seek) { - state->seek = 0; - if (gz_skip(state, state->skip) == -1) - return NULL; - } + /* check that there was no (serious) error */ + if (state->err != Z_OK && state->err != Z_BUF_ERROR && !state->again) + return NULL; + gz_error(state, Z_OK, NULL); - /* copy output bytes up to new line or len - 1, whichever comes first -- - append a terminating zero to the string (we don't check for a zero in - the contents, let the user worry about that) */ + /* process a skip request */ + if (state->skip && gz_skip(state) == -1) + return NULL; + + /* copy output up to a new line, len-1 bytes, or there is no more output, + whichever comes first */ str = buf; left = (unsigned)len - 1; if (left) do { /* assure that something is in the output buffer */ if (state->x.have == 0 && gz_fetch(state) == -1) - return NULL; /* error */ + break; /* error */ if (state->x.have == 0) { /* end of file */ state->past = 1; /* read past end */ break; /* return what we have */ @@ -548,7 +618,9 @@ char * ZEXPORT gzgets(gzFile file, char *buf, int len) { buf += n; } while (left && eol == NULL); - /* return terminated string, or if nothing, end of file */ + /* append a terminating zero to the string (we don't check for a zero in + the contents, let the user worry about that) -- return the terminated + string, or if nothing was read, NULL */ if (buf == str) return NULL; buf[0] = 0; @@ -570,7 +642,7 @@ int ZEXPORT gzdirect(gzFile file) { (void)gz_look(state); /* return 1 if transparent, 0 if processing a gzip stream */ - return state->direct; + return state->direct == 1; } /* -- see zlib.h -- */ @@ -578,12 +650,10 @@ int ZEXPORT gzclose_r(gzFile file) { int ret, err; gz_statep state; - /* get internal structure */ + /* get internal structure and check that it's for reading */ if (file == NULL) return Z_STREAM_ERROR; state = (gz_statep)file; - - /* check that we're reading */ if (state->mode != GZ_READ) return Z_STREAM_ERROR; diff --git a/packages/runtime/vendor/zlib/gzwrite.c b/packages/runtime/vendor/zlib/gzwrite.c index 435b4621b..13a3700a8 100644 --- a/packages/runtime/vendor/zlib/gzwrite.c +++ b/packages/runtime/vendor/zlib/gzwrite.c @@ -1,5 +1,5 @@ /* gzwrite.c -- zlib functions for writing gzip files - * Copyright (C) 2004-2019 Mark Adler + * Copyright (C) 2004-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -74,9 +74,13 @@ local int gz_comp(gz_statep state, int flush) { /* write directly if requested */ if (state->direct) { while (strm->avail_in) { + errno = 0; + state->again = 0; put = strm->avail_in > max ? max : strm->avail_in; - writ = write(state->fd, strm->next_in, put); + writ = (int)write(state->fd, strm->next_in, put); if (writ < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + state->again = 1; gz_error(state, Z_ERRNO, zstrerror()); return -1; } @@ -88,8 +92,9 @@ local int gz_comp(gz_statep state, int flush) { /* check for a pending reset */ if (state->reset) { - /* don't start a new gzip member unless there is data to write */ - if (strm->avail_in == 0) + /* don't start a new gzip member unless there is data to write and + we're not flushing */ + if (strm->avail_in == 0 && flush == Z_NO_FLUSH) return 0; deflateReset(strm); state->reset = 0; @@ -103,10 +108,14 @@ local int gz_comp(gz_statep state, int flush) { if (strm->avail_out == 0 || (flush != Z_NO_FLUSH && (flush != Z_FINISH || ret == Z_STREAM_END))) { while (strm->next_out > state->x.next) { + errno = 0; + state->again = 0; put = strm->next_out - state->x.next > (int)max ? max : (unsigned)(strm->next_out - state->x.next); - writ = write(state->fd, state->x.next, put); + writ = (int)write(state->fd, state->x.next, put); if (writ < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + state->again = 1; gz_error(state, Z_ERRNO, zstrerror()); return -1; } @@ -138,10 +147,12 @@ local int gz_comp(gz_statep state, int flush) { return 0; } -/* Compress len zeros to output. Return -1 on a write error or memory - allocation failure by gz_comp(), or 0 on success. */ -local int gz_zero(gz_statep state, z_off64_t len) { - int first; +/* Compress state->skip (> 0) zeros to output. Return -1 on a write error or + memory allocation failure by gz_comp(), or 0 on success. state->skip is + updated with the number of successfully written zeros, in case there is a + stall on a non-blocking write destination. */ +local int gz_zero(gz_statep state) { + int first, ret; unsigned n; z_streamp strm = &(state->strm); @@ -149,29 +160,34 @@ local int gz_zero(gz_statep state, z_off64_t len) { if (strm->avail_in && gz_comp(state, Z_NO_FLUSH) == -1) return -1; - /* compress len zeros (len guaranteed > 0) */ + /* compress state->skip zeros */ first = 1; - while (len) { - n = GT_OFF(state->size) || (z_off64_t)state->size > len ? - (unsigned)len : state->size; + do { + n = GT_OFF(state->size) || (z_off64_t)state->size > state->skip ? + (unsigned)state->skip : state->size; if (first) { memset(state->in, 0, n); first = 0; } strm->avail_in = n; strm->next_in = state->in; + ret = gz_comp(state, Z_NO_FLUSH); + n -= strm->avail_in; state->x.pos += n; - if (gz_comp(state, Z_NO_FLUSH) == -1) + state->skip -= n; + if (ret == -1) return -1; - len -= n; - } + } while (state->skip); return 0; } /* Write len bytes from buf to file. Return the number of bytes written. If - the returned value is less than len, then there was an error. */ + the returned value is less than len, then there was an error. If the error + was a non-blocking stall, then the number of bytes consumed is returned. + For any other error, 0 is returned. */ local z_size_t gz_write(gz_statep state, voidpc buf, z_size_t len) { z_size_t put = len; + int ret; /* if len is zero, avoid unnecessary operations */ if (len == 0) @@ -182,16 +198,13 @@ local z_size_t gz_write(gz_statep state, voidpc buf, z_size_t len) { return 0; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return 0; - } + if (state->skip && gz_zero(state) == -1) + return 0; /* for small len, copy to input buffer, otherwise compress directly */ if (len < state->size) { /* copy to input buffer, compress when full */ - do { + for (;;) { unsigned have, copy; if (state->strm.avail_in == 0) @@ -206,9 +219,11 @@ local z_size_t gz_write(gz_statep state, voidpc buf, z_size_t len) { state->x.pos += copy; buf = (const char *)buf + copy; len -= copy; - if (len && gz_comp(state, Z_NO_FLUSH) == -1) - return 0; - } while (len); + if (len == 0) + break; + if (gz_comp(state, Z_NO_FLUSH) == -1) + return state->again ? put - len : 0; + } } else { /* consume whatever's left in the input buffer */ @@ -219,13 +234,16 @@ local z_size_t gz_write(gz_statep state, voidpc buf, z_size_t len) { state->strm.next_in = (z_const Bytef *)buf; do { unsigned n = (unsigned)-1; + if (n > len) n = (unsigned)len; state->strm.avail_in = n; + ret = gz_comp(state, Z_NO_FLUSH); + n -= state->strm.avail_in; state->x.pos += n; - if (gz_comp(state, Z_NO_FLUSH) == -1) - return 0; len -= n; + if (ret == -1) + return state->again ? put - len : 0; } while (len); } @@ -242,9 +260,10 @@ int ZEXPORT gzwrite(gzFile file, voidpc buf, unsigned len) { return 0; state = (gz_statep)file; - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return 0; + gz_error(state, Z_OK, NULL); /* since an int is returned, make sure len fits in one, otherwise return with an error (this avoids a flaw in the interface) */ @@ -268,9 +287,10 @@ z_size_t ZEXPORT gzfwrite(voidpc buf, z_size_t size, z_size_t nitems, return 0; state = (gz_statep)file; - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return 0; + gz_error(state, Z_OK, NULL); /* compute bytes to read -- error on overflow */ len = nitems * size; @@ -296,16 +316,14 @@ int ZEXPORT gzputc(gzFile file, int c) { state = (gz_statep)file; strm = &(state->strm); - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return -1; + gz_error(state, Z_OK, NULL); /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return -1; - } + if (state->skip && gz_zero(state) == -1) + return -1; /* try writing to input buffer for speed (state->size == 0 if buffer not initialized) */ @@ -338,9 +356,10 @@ int ZEXPORT gzputs(gzFile file, const char *s) { return -1; state = (gz_statep)file; - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return -1; + gz_error(state, Z_OK, NULL); /* write string */ len = strlen(s); @@ -349,16 +368,47 @@ int ZEXPORT gzputs(gzFile file, const char *s) { return -1; } put = gz_write(state, s, len); - return put < len ? -1 : (int)len; + return len && put == 0 ? -1 : (int)put; } +#if (((!defined(STDC) && !defined(Z_HAVE_STDARG_H)) || !defined(NO_vsnprintf)) && \ + (defined(STDC) || defined(Z_HAVE_STDARG_H) || !defined(NO_snprintf))) || \ + defined(ZLIB_INSECURE) +/* If the second half of the input buffer is occupied, write out the contents. + If there is any input remaining due to a non-blocking stall on write, move + it to the start of the buffer. Return true if this did not open up the + second half of the buffer. state->err should be checked after this to + handle a gz_comp() error. */ +local int gz_vacate(gz_statep state) { + z_streamp strm; + + strm = &(state->strm); + if (strm->next_in + strm->avail_in <= state->in + state->size) + return 0; + (void)gz_comp(state, Z_NO_FLUSH); + if (strm->avail_in == 0) { + strm->next_in = state->in; + return 0; + } + memmove(state->in, strm->next_in, strm->avail_in); + strm->next_in = state->in; + return strm->avail_in > state->size; +} +#endif + #if defined(STDC) || defined(Z_HAVE_STDARG_H) #include /* -- see zlib.h -- */ int ZEXPORTVA gzvprintf(gzFile file, const char *format, va_list va) { - int len; - unsigned left; +#if defined(NO_vsnprintf) && !defined(ZLIB_INSECURE) +#warning "vsnprintf() not available -- gzprintf() stub returns Z_STREAM_ERROR" +#warning "you can recompile with ZLIB_INSECURE defined to use vsprintf()" + /* prevent use of insecure vsprintf(), unless purposefully requested */ + (void)file, (void)format, (void)va; + return Z_STREAM_ERROR; +#else + int len, ret; char *next; gz_statep state; z_streamp strm; @@ -369,24 +419,34 @@ int ZEXPORTVA gzvprintf(gzFile file, const char *format, va_list va) { state = (gz_statep)file; strm = &(state->strm); - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return Z_STREAM_ERROR; + gz_error(state, Z_OK, NULL); /* make sure we have some buffer space */ if (state->size == 0 && gz_init(state) == -1) return state->err; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return state->err; - } + if (state->skip && gz_zero(state) == -1) + return state->err; /* do the printf() into the input buffer, put length in len -- the input - buffer is double-sized just for this function, so there is guaranteed to - be state->size bytes available after the current contents */ + buffer is double-sized just for this function, so there should be + state->size bytes available after the current contents */ + ret = gz_vacate(state); + if (state->err) { + if (ret && state->again) { + /* There was a non-blocking stall on write, resulting in the part + of the second half of the output buffer being occupied. Return + a Z_BUF_ERROR to let the application know that this gzprintf() + needs to be retried. */ + gz_error(state, Z_BUF_ERROR, "stalled write on gzprintf"); + } + if (!state->again) + return state->err; + } if (strm->avail_in == 0) strm->next_in = state->in; next = (char *)(state->in + (strm->next_in - state->in) + strm->avail_in); @@ -412,19 +472,16 @@ int ZEXPORTVA gzvprintf(gzFile file, const char *format, va_list va) { if (len == 0 || (unsigned)len >= state->size || next[state->size - 1] != 0) return 0; - /* update buffer and position, compress first half if past that */ + /* update buffer and position */ strm->avail_in += (unsigned)len; state->x.pos += len; - if (strm->avail_in >= state->size) { - left = strm->avail_in - state->size; - strm->avail_in = state->size; - if (gz_comp(state, Z_NO_FLUSH) == -1) - return state->err; - memmove(state->in, state->in + state->size, left); - strm->next_in = state->in; - strm->avail_in = left; - } + + /* write out buffer if more than half is occupied */ + ret = gz_vacate(state); + if (state->err && !state->again) + return state->err; return len; +#endif } int ZEXPORTVA gzprintf(gzFile file, const char *format, ...) { @@ -444,6 +501,17 @@ int ZEXPORTVA gzprintf(gzFile file, const char *format, int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12, int a13, int a14, int a15, int a16, int a17, int a18, int a19, int a20) { +#if defined(NO_snprintf) && !defined(ZLIB_INSECURE) +#warning "snprintf() not available -- gzprintf() stub returns Z_STREAM_ERROR" +#warning "you can recompile with ZLIB_INSECURE defined to use sprintf()" + /* prevent use of insecure sprintf(), unless purposefully requested */ + (void)file, (void)format, (void)a1, (void)a2, (void)a3, (void)a4, (void)a5, + (void)a6, (void)a7, (void)a8, (void)a9, (void)a10, (void)a11, (void)a12, + (void)a13, (void)a14, (void)a15, (void)a16, (void)a17, (void)a18, + (void)a19, (void)a20; + return Z_STREAM_ERROR; +#else + int ret; unsigned len, left; char *next; gz_statep state; @@ -459,24 +527,34 @@ int ZEXPORTVA gzprintf(gzFile file, const char *format, int a1, int a2, int a3, if (sizeof(int) != sizeof(void *)) return Z_STREAM_ERROR; - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return Z_STREAM_ERROR; + gz_error(state, Z_OK, NULL); /* make sure we have some buffer space */ if (state->size == 0 && gz_init(state) == -1) - return state->error; + return state->err; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return state->error; - } + if (state->skip && gz_zero(state) == -1) + return state->err; /* do the printf() into the input buffer, put length in len -- the input buffer is double-sized just for this function, so there is guaranteed to be state->size bytes available after the current contents */ + ret = gz_vacate(state); + if (state->err) { + if (ret && state->again) { + /* There was a non-blocking stall on write, resulting in the part + of the second half of the output buffer being occupied. Return + a Z_BUF_ERROR to let the application know that this gzprintf() + needs to be retried. */ + gz_error(state, Z_BUF_ERROR, "stalled write on gzprintf"); + } + if (!state->again) + return state->err; + } if (strm->avail_in == 0) strm->next_in = state->in; next = (char *)(strm->next_in + strm->avail_in); @@ -510,16 +588,13 @@ int ZEXPORTVA gzprintf(gzFile file, const char *format, int a1, int a2, int a3, /* update buffer and position, compress first half if past that */ strm->avail_in += len; state->x.pos += len; - if (strm->avail_in >= state->size) { - left = strm->avail_in - state->size; - strm->avail_in = state->size; - if (gz_comp(state, Z_NO_FLUSH) == -1) - return state->err; - memmove(state->in, state->in + state->size, left); - strm->next_in = state->in; - strm->avail_in = left; - } + + /* write out buffer if more than half is occupied */ + ret = gz_vacate(state); + if (state->err && !state->again) + return state->err; return (int)len; +#endif } #endif @@ -533,20 +608,18 @@ int ZEXPORT gzflush(gzFile file, int flush) { return Z_STREAM_ERROR; state = (gz_statep)file; - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK) + /* check that we're writing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again)) return Z_STREAM_ERROR; + gz_error(state, Z_OK, NULL); /* check flush parameter */ if (flush < 0 || flush > Z_FINISH) return Z_STREAM_ERROR; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return state->err; - } + if (state->skip && gz_zero(state) == -1) + return state->err; /* compress remaining data with requested flush */ (void)gz_comp(state, flush); @@ -564,20 +637,19 @@ int ZEXPORT gzsetparams(gzFile file, int level, int strategy) { state = (gz_statep)file; strm = &(state->strm); - /* check that we're writing and that there's no error */ - if (state->mode != GZ_WRITE || state->err != Z_OK || state->direct) + /* check that we're compressing and that there's no (serious) error */ + if (state->mode != GZ_WRITE || (state->err != Z_OK && !state->again) || + state->direct) return Z_STREAM_ERROR; + gz_error(state, Z_OK, NULL); /* if no change is requested, then do nothing */ if (level == state->level && strategy == state->strategy) return Z_OK; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - return state->err; - } + if (state->skip && gz_zero(state) == -1) + return state->err; /* change compression parameters for subsequent input */ if (state->size) { @@ -606,11 +678,8 @@ int ZEXPORT gzclose_w(gzFile file) { return Z_STREAM_ERROR; /* check for seek request */ - if (state->seek) { - state->seek = 0; - if (gz_zero(state, state->skip) == -1) - ret = state->err; - } + if (state->skip && gz_zero(state) == -1) + ret = state->err; /* flush, free memory, and close file */ if (gz_comp(state, Z_FINISH) == -1) diff --git a/packages/runtime/vendor/zlib/infback.c b/packages/runtime/vendor/zlib/infback.c index e7b25b307..7ffb7b044 100644 --- a/packages/runtime/vendor/zlib/infback.c +++ b/packages/runtime/vendor/zlib/infback.c @@ -1,5 +1,5 @@ /* infback.c -- inflate using a call-back interface - * Copyright (C) 1995-2022 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -46,7 +46,7 @@ int ZEXPORT inflateBackInit_(z_streamp strm, int windowBits, #ifdef Z_SOLO return Z_STREAM_ERROR; #else - strm->zfree = zcfree; + strm->zfree = zcfree; #endif state = (struct inflate_state FAR *)ZALLOC(strm, 1, sizeof(struct inflate_state)); @@ -63,57 +63,6 @@ int ZEXPORT inflateBackInit_(z_streamp strm, int windowBits, return Z_OK; } -/* - Return state with length and distance decoding tables and index sizes set to - fixed code decoding. Normally this returns fixed tables from inffixed.h. - If BUILDFIXED is defined, then instead this routine builds the tables the - first time it's called, and returns those tables the first time and - thereafter. This reduces the size of the code by about 2K bytes, in - exchange for a little execution time. However, BUILDFIXED should not be - used for threaded applications, since the rewriting of the tables and virgin - may not be thread-safe. - */ -local void fixedtables(struct inflate_state FAR *state) { -#ifdef BUILDFIXED - static int virgin = 1; - static code *lenfix, *distfix; - static code fixed[544]; - - /* build fixed huffman tables if first call (may not be thread safe) */ - if (virgin) { - unsigned sym, bits; - static code *next; - - /* literal/length table */ - sym = 0; - while (sym < 144) state->lens[sym++] = 8; - while (sym < 256) state->lens[sym++] = 9; - while (sym < 280) state->lens[sym++] = 7; - while (sym < 288) state->lens[sym++] = 8; - next = fixed; - lenfix = next; - bits = 9; - inflate_table(LENS, state->lens, 288, &(next), &(bits), state->work); - - /* distance table */ - sym = 0; - while (sym < 32) state->lens[sym++] = 5; - distfix = next; - bits = 5; - inflate_table(DISTS, state->lens, 32, &(next), &(bits), state->work); - - /* do this just once */ - virgin = 0; - } -#else /* !BUILDFIXED */ -# include "inffixed.h" -#endif /* BUILDFIXED */ - state->lencode = lenfix; - state->lenbits = 9; - state->distcode = distfix; - state->distbits = 5; -} - /* Macros for inflateBack(): */ /* Load returned state from inflate_fast() */ @@ -293,7 +242,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, state->mode = STORED; break; case 1: /* fixed block */ - fixedtables(state); + inflate_fixed(state); Tracev((stderr, "inflate: fixed codes block%s\n", state->last ? " (last)" : "")); state->mode = LEN; /* decode codes */ @@ -303,8 +252,8 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, state->last ? " (last)" : "")); state->mode = TABLE; break; - case 3: - strm->msg = (char *)"invalid block type"; + default: + strm->msg = (z_const char *)"invalid block type"; state->mode = BAD; } DROPBITS(2); @@ -315,7 +264,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, BYTEBITS(); /* go to byte boundary */ NEEDBITS(32); if ((hold & 0xffff) != ((hold >> 16) ^ 0xffff)) { - strm->msg = (char *)"invalid stored block lengths"; + strm->msg = (z_const char *)"invalid stored block lengths"; state->mode = BAD; break; } @@ -353,7 +302,8 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, DROPBITS(4); #ifndef PKZIP_BUG_WORKAROUND if (state->nlen > 286 || state->ndist > 30) { - strm->msg = (char *)"too many length or distance symbols"; + strm->msg = (z_const char *) + "too many length or distance symbols"; state->mode = BAD; break; } @@ -375,7 +325,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, ret = inflate_table(CODES, state->lens, 19, &(state->next), &(state->lenbits), state->work); if (ret) { - strm->msg = (char *)"invalid code lengths set"; + strm->msg = (z_const char *)"invalid code lengths set"; state->mode = BAD; break; } @@ -398,7 +348,8 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, NEEDBITS(here.bits + 2); DROPBITS(here.bits); if (state->have == 0) { - strm->msg = (char *)"invalid bit length repeat"; + strm->msg = (z_const char *) + "invalid bit length repeat"; state->mode = BAD; break; } @@ -421,7 +372,8 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, DROPBITS(7); } if (state->have + copy > state->nlen + state->ndist) { - strm->msg = (char *)"invalid bit length repeat"; + strm->msg = (z_const char *) + "invalid bit length repeat"; state->mode = BAD; break; } @@ -435,30 +387,31 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, /* check for end-of-block code (better have one) */ if (state->lens[256] == 0) { - strm->msg = (char *)"invalid code -- missing end-of-block"; + strm->msg = (z_const char *) + "invalid code -- missing end-of-block"; state->mode = BAD; break; } /* build code tables -- note: do not change the lenbits or distbits - values here (9 and 6) without reading the comments in inftrees.h + values here (10 and 9) without reading the comments in inftrees.h concerning the ENOUGH constants, which depend on those values */ state->next = state->codes; state->lencode = (code const FAR *)(state->next); - state->lenbits = 9; + state->lenbits = 10; ret = inflate_table(LENS, state->lens, state->nlen, &(state->next), &(state->lenbits), state->work); if (ret) { - strm->msg = (char *)"invalid literal/lengths set"; + strm->msg = (z_const char *)"invalid literal/lengths set"; state->mode = BAD; break; } state->distcode = (code const FAR *)(state->next); - state->distbits = 6; + state->distbits = 9; ret = inflate_table(DISTS, state->lens + state->nlen, state->ndist, &(state->next), &(state->distbits), state->work); if (ret) { - strm->msg = (char *)"invalid distances set"; + strm->msg = (z_const char *)"invalid distances set"; state->mode = BAD; break; } @@ -468,10 +421,9 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, case LEN: /* use inflate_fast() if we have enough input and output */ - if (have >= 6 && left >= 258) { + if (have >= INFLATE_FAST_MIN_INPUT && + left >= INFLATE_FAST_MIN_OUTPUT) { RESTORE(); - if (state->whave < state->wsize) - state->whave = state->wsize - left; inflate_fast(strm, state->wsize); LOAD(); break; @@ -517,7 +469,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, /* invalid code */ if (here.op & 64) { - strm->msg = (char *)"invalid literal/length code"; + strm->msg = (z_const char *)"invalid literal/length code"; state->mode = BAD; break; } @@ -549,7 +501,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, } DROPBITS(here.bits); if (here.op & 64) { - strm->msg = (char *)"invalid distance code"; + strm->msg = (z_const char *)"invalid distance code"; state->mode = BAD; break; } @@ -564,7 +516,7 @@ int ZEXPORT inflateBack(z_streamp strm, in_func in, void FAR *in_desc, } if (state->offset > state->wsize - (state->whave < state->wsize ? left : 0)) { - strm->msg = (char *)"invalid distance too far back"; + strm->msg = (z_const char *)"invalid distance too far back"; state->mode = BAD; break; } diff --git a/packages/runtime/vendor/zlib/inffast.c b/packages/runtime/vendor/zlib/inffast.c index 9354676e7..58fc5afc0 100644 --- a/packages/runtime/vendor/zlib/inffast.c +++ b/packages/runtime/vendor/zlib/inffast.c @@ -1,5 +1,5 @@ /* inffast.c -- fast decoding - * Copyright (C) 1995-2017 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -18,13 +18,13 @@ available, an end-of-block is encountered, or a data error is encountered. When large enough input and output buffers are supplied to inflate(), for example, a 16K input buffer and a 64K output buffer, more than 95% of the - inflate execution time is spent in this routine. + inflate() execution time is spent in this routine. Entry assumptions: state->mode == LEN - strm->avail_in >= 6 - strm->avail_out >= 258 + strm->avail_in >= INFLATE_FAST_MIN_INPUT (6 bytes) + strm->avail_out >= INFLATE_FAST_MIN_OUTPUT (258 bytes) start >= strm->avail_out state->bits < 8 @@ -36,16 +36,20 @@ Notes: + INFLATE_FAST_MIN_INPUT: 6 bytes + - The maximum input bits used by a length/distance pair is 15 bits for the length code, 5 bits for the length extra, 15 bits for the distance code, and 13 bits for the distance extra. This totals 48 bits, or six bytes. Therefore if strm->avail_in >= 6, then there is enough input to avoid checking for available input while decoding. + INFLATE_FAST_MIN_OUTPUT: 258 bytes + - The maximum bytes that a single length/distance pair can output is 258 bytes, which is the maximum length that can be coded. inflate_fast() requires strm->avail_out >= 258 for each loop to avoid checking for - output space. + available output space while decoding. */ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { struct inflate_state FAR *state; @@ -77,10 +81,10 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { /* copy state to local variables */ state = (struct inflate_state FAR *)strm->state; in = strm->next_in; - last = in + (strm->avail_in - 5); + last = in + (strm->avail_in - (INFLATE_FAST_MIN_INPUT - 1)); out = strm->next_out; beg = out - (start - strm->avail_out); - end = out + (strm->avail_out - 257); + end = out + (strm->avail_out - (INFLATE_FAST_MIN_OUTPUT - 1)); #ifdef INFLATE_STRICT dmax = state->dmax; #endif @@ -155,7 +159,8 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { dist += (unsigned)hold & ((1U << op) - 1); #ifdef INFLATE_STRICT if (dist > dmax) { - strm->msg = (char *)"invalid distance too far back"; + strm->msg = (z_const char *) + "invalid distance too far back"; state->mode = BAD; break; } @@ -168,8 +173,8 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { op = dist - op; /* distance back in window */ if (op > whave) { if (state->sane) { - strm->msg = - (char *)"invalid distance too far back"; + strm->msg = (z_const char *) + "invalid distance too far back"; state->mode = BAD; break; } @@ -265,7 +270,7 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { goto dodist; } else { - strm->msg = (char *)"invalid distance code"; + strm->msg = (z_const char *)"invalid distance code"; state->mode = BAD; break; } @@ -280,7 +285,7 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { break; } else { - strm->msg = (char *)"invalid literal/length code"; + strm->msg = (z_const char *)"invalid literal/length code"; state->mode = BAD; break; } @@ -295,9 +300,12 @@ void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { /* update state and return */ strm->next_in = in; strm->next_out = out; - strm->avail_in = (unsigned)(in < last ? 5 + (last - in) : 5 - (in - last)); + strm->avail_in = (unsigned)(in < last ? + (INFLATE_FAST_MIN_INPUT - 1) + (last - in) : + (INFLATE_FAST_MIN_INPUT - 1) - (in - last)); strm->avail_out = (unsigned)(out < end ? - 257 + (end - out) : 257 - (out - end)); + (INFLATE_FAST_MIN_OUTPUT - 1) + (end - out) : + (INFLATE_FAST_MIN_OUTPUT - 1) - (out - end)); state->hold = hold; state->bits = bits; return; diff --git a/packages/runtime/vendor/zlib/inffast.h b/packages/runtime/vendor/zlib/inffast.h index 49c6d156c..9ff6ce3b6 100644 --- a/packages/runtime/vendor/zlib/inffast.h +++ b/packages/runtime/vendor/zlib/inffast.h @@ -8,4 +8,19 @@ subject to change. Applications should only use zlib.h. */ +/* INFLATE_FAST_MIN_INPUT: the minimum number of input bytes needed so that + we can safely call inflate_fast() with only one up-front bounds check. One + length/distance code pair (15 bits for the length code, 5 bits for length + extra, 15 bits for the distance code, 13 bits for distance extra) requires + reading up to 48 input bits (6 bytes). +*/ +#define INFLATE_FAST_MIN_INPUT 6 + +/* INFLATE_FAST_MIN_OUTPUT: the minimum number of output bytes needed so that + we can safely call inflate_fast() with only one up-front bounds check. One + length/distance code pair can output up to 258 bytes, which is the maximum + length that can be coded. + */ +#define INFLATE_FAST_MIN_OUTPUT 258 + void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start); diff --git a/packages/runtime/vendor/zlib/inffixed.h b/packages/runtime/vendor/zlib/inffixed.h index d62832776..05ce88e4f 100644 --- a/packages/runtime/vendor/zlib/inffixed.h +++ b/packages/runtime/vendor/zlib/inffixed.h @@ -1,94 +1,94 @@ - /* inffixed.h -- table for decoding fixed codes - * Generated automatically by makefixed(). - */ +/* inffixed.h -- table for decoding fixed codes + * Generated automatically by makefixed(). + */ - /* WARNING: this file should *not* be used by applications. - It is part of the implementation of this library and is - subject to change. Applications should only use zlib.h. - */ +/* WARNING: this file should *not* be used by applications. + It is part of the implementation of this library and is + subject to change. Applications should only use zlib.h. + */ - static const code lenfix[512] = { - {96,7,0},{0,8,80},{0,8,16},{20,8,115},{18,7,31},{0,8,112},{0,8,48}, - {0,9,192},{16,7,10},{0,8,96},{0,8,32},{0,9,160},{0,8,0},{0,8,128}, - {0,8,64},{0,9,224},{16,7,6},{0,8,88},{0,8,24},{0,9,144},{19,7,59}, - {0,8,120},{0,8,56},{0,9,208},{17,7,17},{0,8,104},{0,8,40},{0,9,176}, - {0,8,8},{0,8,136},{0,8,72},{0,9,240},{16,7,4},{0,8,84},{0,8,20}, - {21,8,227},{19,7,43},{0,8,116},{0,8,52},{0,9,200},{17,7,13},{0,8,100}, - {0,8,36},{0,9,168},{0,8,4},{0,8,132},{0,8,68},{0,9,232},{16,7,8}, - {0,8,92},{0,8,28},{0,9,152},{20,7,83},{0,8,124},{0,8,60},{0,9,216}, - {18,7,23},{0,8,108},{0,8,44},{0,9,184},{0,8,12},{0,8,140},{0,8,76}, - {0,9,248},{16,7,3},{0,8,82},{0,8,18},{21,8,163},{19,7,35},{0,8,114}, - {0,8,50},{0,9,196},{17,7,11},{0,8,98},{0,8,34},{0,9,164},{0,8,2}, - {0,8,130},{0,8,66},{0,9,228},{16,7,7},{0,8,90},{0,8,26},{0,9,148}, - {20,7,67},{0,8,122},{0,8,58},{0,9,212},{18,7,19},{0,8,106},{0,8,42}, - {0,9,180},{0,8,10},{0,8,138},{0,8,74},{0,9,244},{16,7,5},{0,8,86}, - {0,8,22},{64,8,0},{19,7,51},{0,8,118},{0,8,54},{0,9,204},{17,7,15}, - {0,8,102},{0,8,38},{0,9,172},{0,8,6},{0,8,134},{0,8,70},{0,9,236}, - {16,7,9},{0,8,94},{0,8,30},{0,9,156},{20,7,99},{0,8,126},{0,8,62}, - {0,9,220},{18,7,27},{0,8,110},{0,8,46},{0,9,188},{0,8,14},{0,8,142}, - {0,8,78},{0,9,252},{96,7,0},{0,8,81},{0,8,17},{21,8,131},{18,7,31}, - {0,8,113},{0,8,49},{0,9,194},{16,7,10},{0,8,97},{0,8,33},{0,9,162}, - {0,8,1},{0,8,129},{0,8,65},{0,9,226},{16,7,6},{0,8,89},{0,8,25}, - {0,9,146},{19,7,59},{0,8,121},{0,8,57},{0,9,210},{17,7,17},{0,8,105}, - {0,8,41},{0,9,178},{0,8,9},{0,8,137},{0,8,73},{0,9,242},{16,7,4}, - {0,8,85},{0,8,21},{16,8,258},{19,7,43},{0,8,117},{0,8,53},{0,9,202}, - {17,7,13},{0,8,101},{0,8,37},{0,9,170},{0,8,5},{0,8,133},{0,8,69}, - {0,9,234},{16,7,8},{0,8,93},{0,8,29},{0,9,154},{20,7,83},{0,8,125}, - {0,8,61},{0,9,218},{18,7,23},{0,8,109},{0,8,45},{0,9,186},{0,8,13}, - {0,8,141},{0,8,77},{0,9,250},{16,7,3},{0,8,83},{0,8,19},{21,8,195}, - {19,7,35},{0,8,115},{0,8,51},{0,9,198},{17,7,11},{0,8,99},{0,8,35}, - {0,9,166},{0,8,3},{0,8,131},{0,8,67},{0,9,230},{16,7,7},{0,8,91}, - {0,8,27},{0,9,150},{20,7,67},{0,8,123},{0,8,59},{0,9,214},{18,7,19}, - {0,8,107},{0,8,43},{0,9,182},{0,8,11},{0,8,139},{0,8,75},{0,9,246}, - {16,7,5},{0,8,87},{0,8,23},{64,8,0},{19,7,51},{0,8,119},{0,8,55}, - {0,9,206},{17,7,15},{0,8,103},{0,8,39},{0,9,174},{0,8,7},{0,8,135}, - {0,8,71},{0,9,238},{16,7,9},{0,8,95},{0,8,31},{0,9,158},{20,7,99}, - {0,8,127},{0,8,63},{0,9,222},{18,7,27},{0,8,111},{0,8,47},{0,9,190}, - {0,8,15},{0,8,143},{0,8,79},{0,9,254},{96,7,0},{0,8,80},{0,8,16}, - {20,8,115},{18,7,31},{0,8,112},{0,8,48},{0,9,193},{16,7,10},{0,8,96}, - {0,8,32},{0,9,161},{0,8,0},{0,8,128},{0,8,64},{0,9,225},{16,7,6}, - {0,8,88},{0,8,24},{0,9,145},{19,7,59},{0,8,120},{0,8,56},{0,9,209}, - {17,7,17},{0,8,104},{0,8,40},{0,9,177},{0,8,8},{0,8,136},{0,8,72}, - {0,9,241},{16,7,4},{0,8,84},{0,8,20},{21,8,227},{19,7,43},{0,8,116}, - {0,8,52},{0,9,201},{17,7,13},{0,8,100},{0,8,36},{0,9,169},{0,8,4}, - {0,8,132},{0,8,68},{0,9,233},{16,7,8},{0,8,92},{0,8,28},{0,9,153}, - {20,7,83},{0,8,124},{0,8,60},{0,9,217},{18,7,23},{0,8,108},{0,8,44}, - {0,9,185},{0,8,12},{0,8,140},{0,8,76},{0,9,249},{16,7,3},{0,8,82}, - {0,8,18},{21,8,163},{19,7,35},{0,8,114},{0,8,50},{0,9,197},{17,7,11}, - {0,8,98},{0,8,34},{0,9,165},{0,8,2},{0,8,130},{0,8,66},{0,9,229}, - {16,7,7},{0,8,90},{0,8,26},{0,9,149},{20,7,67},{0,8,122},{0,8,58}, - {0,9,213},{18,7,19},{0,8,106},{0,8,42},{0,9,181},{0,8,10},{0,8,138}, - {0,8,74},{0,9,245},{16,7,5},{0,8,86},{0,8,22},{64,8,0},{19,7,51}, - {0,8,118},{0,8,54},{0,9,205},{17,7,15},{0,8,102},{0,8,38},{0,9,173}, - {0,8,6},{0,8,134},{0,8,70},{0,9,237},{16,7,9},{0,8,94},{0,8,30}, - {0,9,157},{20,7,99},{0,8,126},{0,8,62},{0,9,221},{18,7,27},{0,8,110}, - {0,8,46},{0,9,189},{0,8,14},{0,8,142},{0,8,78},{0,9,253},{96,7,0}, - {0,8,81},{0,8,17},{21,8,131},{18,7,31},{0,8,113},{0,8,49},{0,9,195}, - {16,7,10},{0,8,97},{0,8,33},{0,9,163},{0,8,1},{0,8,129},{0,8,65}, - {0,9,227},{16,7,6},{0,8,89},{0,8,25},{0,9,147},{19,7,59},{0,8,121}, - {0,8,57},{0,9,211},{17,7,17},{0,8,105},{0,8,41},{0,9,179},{0,8,9}, - {0,8,137},{0,8,73},{0,9,243},{16,7,4},{0,8,85},{0,8,21},{16,8,258}, - {19,7,43},{0,8,117},{0,8,53},{0,9,203},{17,7,13},{0,8,101},{0,8,37}, - {0,9,171},{0,8,5},{0,8,133},{0,8,69},{0,9,235},{16,7,8},{0,8,93}, - {0,8,29},{0,9,155},{20,7,83},{0,8,125},{0,8,61},{0,9,219},{18,7,23}, - {0,8,109},{0,8,45},{0,9,187},{0,8,13},{0,8,141},{0,8,77},{0,9,251}, - {16,7,3},{0,8,83},{0,8,19},{21,8,195},{19,7,35},{0,8,115},{0,8,51}, - {0,9,199},{17,7,11},{0,8,99},{0,8,35},{0,9,167},{0,8,3},{0,8,131}, - {0,8,67},{0,9,231},{16,7,7},{0,8,91},{0,8,27},{0,9,151},{20,7,67}, - {0,8,123},{0,8,59},{0,9,215},{18,7,19},{0,8,107},{0,8,43},{0,9,183}, - {0,8,11},{0,8,139},{0,8,75},{0,9,247},{16,7,5},{0,8,87},{0,8,23}, - {64,8,0},{19,7,51},{0,8,119},{0,8,55},{0,9,207},{17,7,15},{0,8,103}, - {0,8,39},{0,9,175},{0,8,7},{0,8,135},{0,8,71},{0,9,239},{16,7,9}, - {0,8,95},{0,8,31},{0,9,159},{20,7,99},{0,8,127},{0,8,63},{0,9,223}, - {18,7,27},{0,8,111},{0,8,47},{0,9,191},{0,8,15},{0,8,143},{0,8,79}, - {0,9,255} - }; +static const code lenfix[512] = { + {96,7,0},{0,8,80},{0,8,16},{20,8,115},{18,7,31},{0,8,112},{0,8,48}, + {0,9,192},{16,7,10},{0,8,96},{0,8,32},{0,9,160},{0,8,0},{0,8,128}, + {0,8,64},{0,9,224},{16,7,6},{0,8,88},{0,8,24},{0,9,144},{19,7,59}, + {0,8,120},{0,8,56},{0,9,208},{17,7,17},{0,8,104},{0,8,40},{0,9,176}, + {0,8,8},{0,8,136},{0,8,72},{0,9,240},{16,7,4},{0,8,84},{0,8,20}, + {21,8,227},{19,7,43},{0,8,116},{0,8,52},{0,9,200},{17,7,13},{0,8,100}, + {0,8,36},{0,9,168},{0,8,4},{0,8,132},{0,8,68},{0,9,232},{16,7,8}, + {0,8,92},{0,8,28},{0,9,152},{20,7,83},{0,8,124},{0,8,60},{0,9,216}, + {18,7,23},{0,8,108},{0,8,44},{0,9,184},{0,8,12},{0,8,140},{0,8,76}, + {0,9,248},{16,7,3},{0,8,82},{0,8,18},{21,8,163},{19,7,35},{0,8,114}, + {0,8,50},{0,9,196},{17,7,11},{0,8,98},{0,8,34},{0,9,164},{0,8,2}, + {0,8,130},{0,8,66},{0,9,228},{16,7,7},{0,8,90},{0,8,26},{0,9,148}, + {20,7,67},{0,8,122},{0,8,58},{0,9,212},{18,7,19},{0,8,106},{0,8,42}, + {0,9,180},{0,8,10},{0,8,138},{0,8,74},{0,9,244},{16,7,5},{0,8,86}, + {0,8,22},{64,8,0},{19,7,51},{0,8,118},{0,8,54},{0,9,204},{17,7,15}, + {0,8,102},{0,8,38},{0,9,172},{0,8,6},{0,8,134},{0,8,70},{0,9,236}, + {16,7,9},{0,8,94},{0,8,30},{0,9,156},{20,7,99},{0,8,126},{0,8,62}, + {0,9,220},{18,7,27},{0,8,110},{0,8,46},{0,9,188},{0,8,14},{0,8,142}, + {0,8,78},{0,9,252},{96,7,0},{0,8,81},{0,8,17},{21,8,131},{18,7,31}, + {0,8,113},{0,8,49},{0,9,194},{16,7,10},{0,8,97},{0,8,33},{0,9,162}, + {0,8,1},{0,8,129},{0,8,65},{0,9,226},{16,7,6},{0,8,89},{0,8,25}, + {0,9,146},{19,7,59},{0,8,121},{0,8,57},{0,9,210},{17,7,17},{0,8,105}, + {0,8,41},{0,9,178},{0,8,9},{0,8,137},{0,8,73},{0,9,242},{16,7,4}, + {0,8,85},{0,8,21},{16,8,258},{19,7,43},{0,8,117},{0,8,53},{0,9,202}, + {17,7,13},{0,8,101},{0,8,37},{0,9,170},{0,8,5},{0,8,133},{0,8,69}, + {0,9,234},{16,7,8},{0,8,93},{0,8,29},{0,9,154},{20,7,83},{0,8,125}, + {0,8,61},{0,9,218},{18,7,23},{0,8,109},{0,8,45},{0,9,186},{0,8,13}, + {0,8,141},{0,8,77},{0,9,250},{16,7,3},{0,8,83},{0,8,19},{21,8,195}, + {19,7,35},{0,8,115},{0,8,51},{0,9,198},{17,7,11},{0,8,99},{0,8,35}, + {0,9,166},{0,8,3},{0,8,131},{0,8,67},{0,9,230},{16,7,7},{0,8,91}, + {0,8,27},{0,9,150},{20,7,67},{0,8,123},{0,8,59},{0,9,214},{18,7,19}, + {0,8,107},{0,8,43},{0,9,182},{0,8,11},{0,8,139},{0,8,75},{0,9,246}, + {16,7,5},{0,8,87},{0,8,23},{64,8,0},{19,7,51},{0,8,119},{0,8,55}, + {0,9,206},{17,7,15},{0,8,103},{0,8,39},{0,9,174},{0,8,7},{0,8,135}, + {0,8,71},{0,9,238},{16,7,9},{0,8,95},{0,8,31},{0,9,158},{20,7,99}, + {0,8,127},{0,8,63},{0,9,222},{18,7,27},{0,8,111},{0,8,47},{0,9,190}, + {0,8,15},{0,8,143},{0,8,79},{0,9,254},{96,7,0},{0,8,80},{0,8,16}, + {20,8,115},{18,7,31},{0,8,112},{0,8,48},{0,9,193},{16,7,10},{0,8,96}, + {0,8,32},{0,9,161},{0,8,0},{0,8,128},{0,8,64},{0,9,225},{16,7,6}, + {0,8,88},{0,8,24},{0,9,145},{19,7,59},{0,8,120},{0,8,56},{0,9,209}, + {17,7,17},{0,8,104},{0,8,40},{0,9,177},{0,8,8},{0,8,136},{0,8,72}, + {0,9,241},{16,7,4},{0,8,84},{0,8,20},{21,8,227},{19,7,43},{0,8,116}, + {0,8,52},{0,9,201},{17,7,13},{0,8,100},{0,8,36},{0,9,169},{0,8,4}, + {0,8,132},{0,8,68},{0,9,233},{16,7,8},{0,8,92},{0,8,28},{0,9,153}, + {20,7,83},{0,8,124},{0,8,60},{0,9,217},{18,7,23},{0,8,108},{0,8,44}, + {0,9,185},{0,8,12},{0,8,140},{0,8,76},{0,9,249},{16,7,3},{0,8,82}, + {0,8,18},{21,8,163},{19,7,35},{0,8,114},{0,8,50},{0,9,197},{17,7,11}, + {0,8,98},{0,8,34},{0,9,165},{0,8,2},{0,8,130},{0,8,66},{0,9,229}, + {16,7,7},{0,8,90},{0,8,26},{0,9,149},{20,7,67},{0,8,122},{0,8,58}, + {0,9,213},{18,7,19},{0,8,106},{0,8,42},{0,9,181},{0,8,10},{0,8,138}, + {0,8,74},{0,9,245},{16,7,5},{0,8,86},{0,8,22},{64,8,0},{19,7,51}, + {0,8,118},{0,8,54},{0,9,205},{17,7,15},{0,8,102},{0,8,38},{0,9,173}, + {0,8,6},{0,8,134},{0,8,70},{0,9,237},{16,7,9},{0,8,94},{0,8,30}, + {0,9,157},{20,7,99},{0,8,126},{0,8,62},{0,9,221},{18,7,27},{0,8,110}, + {0,8,46},{0,9,189},{0,8,14},{0,8,142},{0,8,78},{0,9,253},{96,7,0}, + {0,8,81},{0,8,17},{21,8,131},{18,7,31},{0,8,113},{0,8,49},{0,9,195}, + {16,7,10},{0,8,97},{0,8,33},{0,9,163},{0,8,1},{0,8,129},{0,8,65}, + {0,9,227},{16,7,6},{0,8,89},{0,8,25},{0,9,147},{19,7,59},{0,8,121}, + {0,8,57},{0,9,211},{17,7,17},{0,8,105},{0,8,41},{0,9,179},{0,8,9}, + {0,8,137},{0,8,73},{0,9,243},{16,7,4},{0,8,85},{0,8,21},{16,8,258}, + {19,7,43},{0,8,117},{0,8,53},{0,9,203},{17,7,13},{0,8,101},{0,8,37}, + {0,9,171},{0,8,5},{0,8,133},{0,8,69},{0,9,235},{16,7,8},{0,8,93}, + {0,8,29},{0,9,155},{20,7,83},{0,8,125},{0,8,61},{0,9,219},{18,7,23}, + {0,8,109},{0,8,45},{0,9,187},{0,8,13},{0,8,141},{0,8,77},{0,9,251}, + {16,7,3},{0,8,83},{0,8,19},{21,8,195},{19,7,35},{0,8,115},{0,8,51}, + {0,9,199},{17,7,11},{0,8,99},{0,8,35},{0,9,167},{0,8,3},{0,8,131}, + {0,8,67},{0,9,231},{16,7,7},{0,8,91},{0,8,27},{0,9,151},{20,7,67}, + {0,8,123},{0,8,59},{0,9,215},{18,7,19},{0,8,107},{0,8,43},{0,9,183}, + {0,8,11},{0,8,139},{0,8,75},{0,9,247},{16,7,5},{0,8,87},{0,8,23}, + {64,8,0},{19,7,51},{0,8,119},{0,8,55},{0,9,207},{17,7,15},{0,8,103}, + {0,8,39},{0,9,175},{0,8,7},{0,8,135},{0,8,71},{0,9,239},{16,7,9}, + {0,8,95},{0,8,31},{0,9,159},{20,7,99},{0,8,127},{0,8,63},{0,9,223}, + {18,7,27},{0,8,111},{0,8,47},{0,9,191},{0,8,15},{0,8,143},{0,8,79}, + {0,9,255} +}; - static const code distfix[32] = { - {16,5,1},{23,5,257},{19,5,17},{27,5,4097},{17,5,5},{25,5,1025}, - {21,5,65},{29,5,16385},{16,5,3},{24,5,513},{20,5,33},{28,5,8193}, - {18,5,9},{26,5,2049},{22,5,129},{64,5,0},{16,5,2},{23,5,385}, - {19,5,25},{27,5,6145},{17,5,7},{25,5,1537},{21,5,97},{29,5,24577}, - {16,5,4},{24,5,769},{20,5,49},{28,5,12289},{18,5,13},{26,5,3073}, - {22,5,193},{64,5,0} - }; +static const code distfix[32] = { + {16,5,1},{23,5,257},{19,5,17},{27,5,4097},{17,5,5},{25,5,1025}, + {21,5,65},{29,5,16385},{16,5,3},{24,5,513},{20,5,33},{28,5,8193}, + {18,5,9},{26,5,2049},{22,5,129},{64,5,0},{16,5,2},{23,5,385}, + {19,5,25},{27,5,6145},{17,5,7},{25,5,1537},{21,5,97},{29,5,24577}, + {16,5,4},{24,5,769},{20,5,49},{28,5,12289},{18,5,13},{26,5,3073}, + {22,5,193},{64,5,0} +}; diff --git a/packages/runtime/vendor/zlib/inflate.c b/packages/runtime/vendor/zlib/inflate.c index 94ecff015..0742ce10e 100644 --- a/packages/runtime/vendor/zlib/inflate.c +++ b/packages/runtime/vendor/zlib/inflate.c @@ -1,5 +1,5 @@ /* inflate.c -- zlib decompression - * Copyright (C) 1995-2022 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -85,12 +85,6 @@ #include "inflate.h" #include "inffast.h" -#ifdef MAKEFIXED -# ifndef BUILDFIXED -# define BUILDFIXED -# endif -#endif - local int inflateStateCheck(z_streamp strm) { struct inflate_state FAR *state; if (strm == Z_NULL || @@ -110,6 +104,7 @@ int ZEXPORT inflateResetKeep(z_streamp strm) { state = (struct inflate_state FAR *)strm->state; strm->total_in = strm->total_out = state->total = 0; strm->msg = Z_NULL; + strm->data_type = 0; if (state->wrap) /* to support ill-conceived Java test suite */ strm->adler = state->wrap & 1; state->mode = HEAD; @@ -202,11 +197,13 @@ int ZEXPORT inflateInit2_(z_streamp strm, int windowBits, state = (struct inflate_state FAR *) ZALLOC(strm, 1, sizeof(struct inflate_state)); if (state == Z_NULL) return Z_MEM_ERROR; + zmemzero(state, sizeof(struct inflate_state)); Tracev((stderr, "inflate: allocated\n")); strm->state = (struct internal_state FAR *)state; state->strm = strm; state->window = Z_NULL; state->mode = HEAD; /* to pass state test in inflateReset2() */ + state->check = 1L; /* 1L is the result of adler32() zero length data */ ret = inflateReset2(strm, windowBits); if (ret != Z_OK) { ZFREE(strm, state); @@ -234,123 +231,11 @@ int ZEXPORT inflatePrime(z_streamp strm, int bits, int value) { } if (bits > 16 || state->bits + (uInt)bits > 32) return Z_STREAM_ERROR; value &= (1L << bits) - 1; - state->hold += (unsigned)value << state->bits; + state->hold += (unsigned long)value << state->bits; state->bits += (uInt)bits; return Z_OK; } -/* - Return state with length and distance decoding tables and index sizes set to - fixed code decoding. Normally this returns fixed tables from inffixed.h. - If BUILDFIXED is defined, then instead this routine builds the tables the - first time it's called, and returns those tables the first time and - thereafter. This reduces the size of the code by about 2K bytes, in - exchange for a little execution time. However, BUILDFIXED should not be - used for threaded applications, since the rewriting of the tables and virgin - may not be thread-safe. - */ -local void fixedtables(struct inflate_state FAR *state) { -#ifdef BUILDFIXED - static int virgin = 1; - static code *lenfix, *distfix; - static code fixed[544]; - - /* build fixed huffman tables if first call (may not be thread safe) */ - if (virgin) { - unsigned sym, bits; - static code *next; - - /* literal/length table */ - sym = 0; - while (sym < 144) state->lens[sym++] = 8; - while (sym < 256) state->lens[sym++] = 9; - while (sym < 280) state->lens[sym++] = 7; - while (sym < 288) state->lens[sym++] = 8; - next = fixed; - lenfix = next; - bits = 9; - inflate_table(LENS, state->lens, 288, &(next), &(bits), state->work); - - /* distance table */ - sym = 0; - while (sym < 32) state->lens[sym++] = 5; - distfix = next; - bits = 5; - inflate_table(DISTS, state->lens, 32, &(next), &(bits), state->work); - - /* do this just once */ - virgin = 0; - } -#else /* !BUILDFIXED */ -# include "inffixed.h" -#endif /* BUILDFIXED */ - state->lencode = lenfix; - state->lenbits = 9; - state->distcode = distfix; - state->distbits = 5; -} - -#ifdef MAKEFIXED -#include - -/* - Write out the inffixed.h that is #include'd above. Defining MAKEFIXED also - defines BUILDFIXED, so the tables are built on the fly. makefixed() writes - those tables to stdout, which would be piped to inffixed.h. A small program - can simply call makefixed to do this: - - void makefixed(void); - - int main(void) - { - makefixed(); - return 0; - } - - Then that can be linked with zlib built with MAKEFIXED defined and run: - - a.out > inffixed.h - */ -void makefixed(void) -{ - unsigned low, size; - struct inflate_state state; - - fixedtables(&state); - puts(" /* inffixed.h -- table for decoding fixed codes"); - puts(" * Generated automatically by makefixed()."); - puts(" */"); - puts(""); - puts(" /* WARNING: this file should *not* be used by applications."); - puts(" It is part of the implementation of this library and is"); - puts(" subject to change. Applications should only use zlib.h."); - puts(" */"); - puts(""); - size = 1U << 9; - printf(" static const code lenfix[%u] = {", size); - low = 0; - for (;;) { - if ((low % 7) == 0) printf("\n "); - printf("{%u,%u,%d}", (low & 127) == 99 ? 64 : state.lencode[low].op, - state.lencode[low].bits, state.lencode[low].val); - if (++low == size) break; - putchar(','); - } - puts("\n };"); - size = 1U << 5; - printf("\n static const code distfix[%u] = {", size); - low = 0; - for (;;) { - if ((low % 6) == 0) printf("\n "); - printf("{%u,%u,%d}", state.distcode[low].op, state.distcode[low].bits, - state.distcode[low].val); - if (++low == size) break; - putchar(','); - } - puts("\n };"); -} -#endif /* MAKEFIXED */ - /* Update the window with the last wsize (normally 32K) bytes written before returning. If window does not exist yet, create it. This is only called @@ -642,12 +527,12 @@ int ZEXPORT inflate(z_streamp strm, int flush) { if ( #endif ((BITS(8) << 8) + (hold >> 8)) % 31) { - strm->msg = (char *)"incorrect header check"; + strm->msg = (z_const char *)"incorrect header check"; state->mode = BAD; break; } if (BITS(4) != Z_DEFLATED) { - strm->msg = (char *)"unknown compression method"; + strm->msg = (z_const char *)"unknown compression method"; state->mode = BAD; break; } @@ -656,7 +541,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { if (state->wbits == 0) state->wbits = len; if (len > 15 || len > state->wbits) { - strm->msg = (char *)"invalid window size"; + strm->msg = (z_const char *)"invalid window size"; state->mode = BAD; break; } @@ -672,12 +557,12 @@ int ZEXPORT inflate(z_streamp strm, int flush) { NEEDBITS(16); state->flags = (int)(hold); if ((state->flags & 0xff) != Z_DEFLATED) { - strm->msg = (char *)"unknown compression method"; + strm->msg = (z_const char *)"unknown compression method"; state->mode = BAD; break; } if (state->flags & 0xe000) { - strm->msg = (char *)"unknown header flags set"; + strm->msg = (z_const char *)"unknown header flags set"; state->mode = BAD; break; } @@ -793,7 +678,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { if (state->flags & 0x0200) { NEEDBITS(16); if ((state->wrap & 4) && hold != (state->check & 0xffff)) { - strm->msg = (char *)"header crc mismatch"; + strm->msg = (z_const char *)"header crc mismatch"; state->mode = BAD; break; } @@ -840,7 +725,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { state->mode = STORED; break; case 1: /* fixed block */ - fixedtables(state); + inflate_fixed(state); Tracev((stderr, "inflate: fixed codes block%s\n", state->last ? " (last)" : "")); state->mode = LEN_; /* decode codes */ @@ -854,8 +739,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { state->last ? " (last)" : "")); state->mode = TABLE; break; - case 3: - strm->msg = (char *)"invalid block type"; + default: + strm->msg = (z_const char *)"invalid block type"; state->mode = BAD; } DROPBITS(2); @@ -864,7 +749,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { BYTEBITS(); /* go to byte boundary */ NEEDBITS(32); if ((hold & 0xffff) != ((hold >> 16) ^ 0xffff)) { - strm->msg = (char *)"invalid stored block lengths"; + strm->msg = (z_const char *)"invalid stored block lengths"; state->mode = BAD; break; } @@ -905,7 +790,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { DROPBITS(4); #ifndef PKZIP_BUG_WORKAROUND if (state->nlen > 286 || state->ndist > 30) { - strm->msg = (char *)"too many length or distance symbols"; + strm->msg = (z_const char *) + "too many length or distance symbols"; state->mode = BAD; break; } @@ -923,12 +809,12 @@ int ZEXPORT inflate(z_streamp strm, int flush) { while (state->have < 19) state->lens[order[state->have++]] = 0; state->next = state->codes; - state->lencode = (const code FAR *)(state->next); + state->lencode = state->distcode = (const code FAR *)(state->next); state->lenbits = 7; ret = inflate_table(CODES, state->lens, 19, &(state->next), &(state->lenbits), state->work); if (ret) { - strm->msg = (char *)"invalid code lengths set"; + strm->msg = (z_const char *)"invalid code lengths set"; state->mode = BAD; break; } @@ -952,7 +838,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { NEEDBITS(here.bits + 2); DROPBITS(here.bits); if (state->have == 0) { - strm->msg = (char *)"invalid bit length repeat"; + strm->msg = (z_const char *) + "invalid bit length repeat"; state->mode = BAD; break; } @@ -975,7 +862,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { DROPBITS(7); } if (state->have + copy > state->nlen + state->ndist) { - strm->msg = (char *)"invalid bit length repeat"; + strm->msg = (z_const char *) + "invalid bit length repeat"; state->mode = BAD; break; } @@ -989,30 +877,31 @@ int ZEXPORT inflate(z_streamp strm, int flush) { /* check for end-of-block code (better have one) */ if (state->lens[256] == 0) { - strm->msg = (char *)"invalid code -- missing end-of-block"; + strm->msg = (z_const char *) + "invalid code -- missing end-of-block"; state->mode = BAD; break; } /* build code tables -- note: do not change the lenbits or distbits - values here (9 and 6) without reading the comments in inftrees.h + values here (10 and 9) without reading the comments in inftrees.h concerning the ENOUGH constants, which depend on those values */ state->next = state->codes; state->lencode = (const code FAR *)(state->next); - state->lenbits = 9; + state->lenbits = 10; ret = inflate_table(LENS, state->lens, state->nlen, &(state->next), &(state->lenbits), state->work); if (ret) { - strm->msg = (char *)"invalid literal/lengths set"; + strm->msg = (z_const char *)"invalid literal/lengths set"; state->mode = BAD; break; } state->distcode = (const code FAR *)(state->next); - state->distbits = 6; + state->distbits = 9; ret = inflate_table(DISTS, state->lens + state->nlen, state->ndist, &(state->next), &(state->distbits), state->work); if (ret) { - strm->msg = (char *)"invalid distances set"; + strm->msg = (z_const char *)"invalid distances set"; state->mode = BAD; break; } @@ -1024,7 +913,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { state->mode = LEN; /* fallthrough */ case LEN: - if (have >= 6 && left >= 258) { + if (have >= INFLATE_FAST_MIN_INPUT && + left >= INFLATE_FAST_MIN_OUTPUT) { RESTORE(); inflate_fast(strm, out); LOAD(); @@ -1066,7 +956,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { break; } if (here.op & 64) { - strm->msg = (char *)"invalid literal/length code"; + strm->msg = (z_const char *)"invalid literal/length code"; state->mode = BAD; break; } @@ -1104,7 +994,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { DROPBITS(here.bits); state->back += here.bits; if (here.op & 64) { - strm->msg = (char *)"invalid distance code"; + strm->msg = (z_const char *)"invalid distance code"; state->mode = BAD; break; } @@ -1121,7 +1011,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { } #ifdef INFLATE_STRICT if (state->offset > state->dmax) { - strm->msg = (char *)"invalid distance too far back"; + strm->msg = (z_const char *)"invalid distance too far back"; state->mode = BAD; break; } @@ -1136,7 +1026,8 @@ int ZEXPORT inflate(z_streamp strm, int flush) { copy = state->offset - copy; if (copy > state->whave) { if (state->sane) { - strm->msg = (char *)"invalid distance too far back"; + strm->msg = (z_const char *) + "invalid distance too far back"; state->mode = BAD; break; } @@ -1195,7 +1086,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { state->flags ? hold : #endif ZSWAP32(hold)) != state->check) { - strm->msg = (char *)"incorrect data check"; + strm->msg = (z_const char *)"incorrect data check"; state->mode = BAD; break; } @@ -1209,7 +1100,7 @@ int ZEXPORT inflate(z_streamp strm, int flush) { if (state->wrap && state->flags) { NEEDBITS(32); if ((state->wrap & 4) && hold != (state->total & 0xffffffff)) { - strm->msg = (char *)"incorrect length check"; + strm->msg = (z_const char *)"incorrect length check"; state->mode = BAD; break; } @@ -1440,7 +1331,6 @@ int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) { struct inflate_state FAR *state; struct inflate_state FAR *copy; unsigned char FAR *window; - unsigned wsize; /* check input */ if (inflateStateCheck(source) || dest == Z_NULL) @@ -1451,6 +1341,7 @@ int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) { copy = (struct inflate_state FAR *) ZALLOC(source, 1, sizeof(struct inflate_state)); if (copy == Z_NULL) return Z_MEM_ERROR; + zmemzero(copy, sizeof(struct inflate_state)); window = Z_NULL; if (state->window != Z_NULL) { window = (unsigned char FAR *) @@ -1462,8 +1353,8 @@ int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) { } /* copy state */ - zmemcpy((voidpf)dest, (voidpf)source, sizeof(z_stream)); - zmemcpy((voidpf)copy, (voidpf)state, sizeof(struct inflate_state)); + zmemcpy(dest, source, sizeof(z_stream)); + zmemcpy(copy, state, sizeof(struct inflate_state)); copy->strm = dest; if (state->lencode >= state->codes && state->lencode <= state->codes + ENOUGH - 1) { @@ -1471,10 +1362,8 @@ int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) { copy->distcode = copy->codes + (state->distcode - state->codes); } copy->next = copy->codes + (state->next - state->codes); - if (window != Z_NULL) { - wsize = 1U << state->wbits; - zmemcpy(window, state->window, wsize); - } + if (window != Z_NULL) + zmemcpy(window, state->window, state->whave); copy->window = window; dest->state = (struct internal_state FAR *)copy; return Z_OK; diff --git a/packages/runtime/vendor/zlib/inflate.h b/packages/runtime/vendor/zlib/inflate.h index f127b6b1f..f758e0dcc 100644 --- a/packages/runtime/vendor/zlib/inflate.h +++ b/packages/runtime/vendor/zlib/inflate.h @@ -100,7 +100,7 @@ struct inflate_state { unsigned char FAR *window; /* allocated sliding window, if needed */ /* bit accumulator */ unsigned long hold; /* input bit accumulator */ - unsigned bits; /* number of bits in "in" */ + unsigned bits; /* number of bits in hold */ /* for string and stored block copying */ unsigned length; /* literal or length of data to copy */ unsigned offset; /* distance back to copy string from */ diff --git a/packages/runtime/vendor/zlib/inftrees.c b/packages/runtime/vendor/zlib/inftrees.c index 98cfe1644..99d8d7730 100644 --- a/packages/runtime/vendor/zlib/inftrees.c +++ b/packages/runtime/vendor/zlib/inftrees.c @@ -1,15 +1,29 @@ /* inftrees.c -- generate Huffman trees for efficient decoding - * Copyright (C) 1995-2024 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ +#ifdef MAKEFIXED +# ifndef BUILDFIXED +# define BUILDFIXED +# endif +#endif +#ifdef BUILDFIXED +# define Z_ONCE +#endif + #include "zutil.h" #include "inftrees.h" +#include "inflate.h" + +#ifndef NULL +# define NULL 0 +#endif #define MAXBITS 15 const char inflate_copyright[] = - " inflate 1.3.1 Copyright 1995-2024 Mark Adler "; + " inflate 1.3.2.1 Copyright 1995-2026 Mark Adler "; /* If you use the zlib library in a product, an acknowledgment is welcome in the documentation of your product. If for some reason you cannot @@ -47,9 +61,9 @@ int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, unsigned mask; /* mask for low root bits */ code here; /* table entry for duplication */ code FAR *next; /* next available space in table */ - const unsigned short FAR *base; /* base value table to use */ - const unsigned short FAR *extra; /* extra bits table to use */ - unsigned match; /* use base and extra for symbol >= match */ + const unsigned short FAR *base = NULL; /* base value table to use */ + const unsigned short FAR *extra = NULL; /* extra bits table to use */ + unsigned match = 0; /* use base and extra for symbol >= match */ unsigned short count[MAXBITS+1]; /* number of codes of each length */ unsigned short offs[MAXBITS+1]; /* offsets in table for each length */ static const unsigned short lbase[31] = { /* Length codes 257..285 base */ @@ -57,7 +71,7 @@ int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0}; static const unsigned short lext[31] = { /* Length codes 257..285 extra */ 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, - 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 203, 77}; + 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 68, 193}; static const unsigned short dbase[32] = { /* Distance codes 0..29 base */ 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, @@ -175,7 +189,6 @@ int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, /* set up for code type */ switch (type) { case CODES: - base = extra = work; /* dummy value--not used */ match = 20; break; case LENS: @@ -183,10 +196,9 @@ int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, extra = lext; match = 257; break; - default: /* DISTS */ + case DISTS: base = dbase; extra = dext; - match = 0; } /* initialize state for loop */ @@ -297,3 +309,116 @@ int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, *bits = root; return 0; } + +#ifdef BUILDFIXED +/* + If this is compiled with BUILDFIXED defined, and if inflate will be used in + multiple threads, and if atomics are not available, then inflate() must be + called with a fixed block (e.g. 0x03 0x00) to initialize the tables and must + return before any other threads are allowed to call inflate. + */ + +static code *lenfix, *distfix; +static code fixed[544]; + +/* State for z_once(). */ +local z_once_t built = Z_ONCE_INIT; + +local void buildtables(void) { + unsigned sym, bits; + static code *next; + unsigned short lens[288], work[288]; + + /* literal/length table */ + sym = 0; + while (sym < 144) lens[sym++] = 8; + while (sym < 256) lens[sym++] = 9; + while (sym < 280) lens[sym++] = 7; + while (sym < 288) lens[sym++] = 8; + next = fixed; + lenfix = next; + bits = 9; + inflate_table(LENS, lens, 288, &(next), &(bits), work); + + /* distance table */ + sym = 0; + while (sym < 32) lens[sym++] = 5; + distfix = next; + bits = 5; + inflate_table(DISTS, lens, 32, &(next), &(bits), work); +} +#else /* !BUILDFIXED */ +# include "inffixed.h" +#endif /* BUILDFIXED */ + +/* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications if atomics are not available, as it will + not be thread-safe. + */ +void inflate_fixed(struct inflate_state FAR *state) { +#ifdef BUILDFIXED + z_once(&built, buildtables); +#endif /* BUILDFIXED */ + state->lencode = lenfix; + state->lenbits = 9; + state->distcode = distfix; + state->distbits = 5; +} + +#ifdef MAKEFIXED +#include + +/* + Write out the inffixed.h that will be #include'd above. Defining MAKEFIXED + also defines BUILDFIXED, so the tables are built on the fly. main() writes + those tables to stdout, which would directed to inffixed.h. Compile this + along with zutil.c: + + cc -DMAKEFIXED -o fix inftrees.c zutil.c + ./fix > inffixed.h + */ +int main(void) { + unsigned low, size; + struct inflate_state state; + + inflate_fixed(&state); + puts("/* inffixed.h -- table for decoding fixed codes"); + puts(" * Generated automatically by makefixed()."); + puts(" */"); + puts(""); + puts("/* WARNING: this file should *not* be used by applications."); + puts(" It is part of the implementation of this library and is"); + puts(" subject to change. Applications should only use zlib.h."); + puts(" */"); + puts(""); + size = 1U << 9; + printf("static const code lenfix[%u] = {", size); + low = 0; + for (;;) { + if ((low % 7) == 0) printf("\n "); + printf("{%u,%u,%d}", (low & 127) == 99 ? 64 : state.lencode[low].op, + state.lencode[low].bits, state.lencode[low].val); + if (++low == size) break; + putchar(','); + } + puts("\n};"); + size = 1U << 5; + printf("\nstatic const code distfix[%u] = {", size); + low = 0; + for (;;) { + if ((low % 6) == 0) printf("\n "); + printf("{%u,%u,%d}", state.distcode[low].op, state.distcode[low].bits, + state.distcode[low].val); + if (++low == size) break; + putchar(','); + } + puts("\n};"); + return 0; +} +#endif /* MAKEFIXED */ diff --git a/packages/runtime/vendor/zlib/inftrees.h b/packages/runtime/vendor/zlib/inftrees.h index 396f74b5d..c572f49d5 100644 --- a/packages/runtime/vendor/zlib/inftrees.h +++ b/packages/runtime/vendor/zlib/inftrees.h @@ -1,5 +1,5 @@ /* inftrees.h -- header to use inftrees.c - * Copyright (C) 1995-2005, 2010 Mark Adler + * Copyright (C) 1995-2026 Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -36,17 +36,17 @@ typedef struct { */ /* Maximum size of the dynamic table. The maximum number of code structures is - 1444, which is the sum of 852 for literal/length codes and 592 for distance + 1924, which is the sum of 1332 for literal/length codes and 592 for distance codes. These values were found by exhaustive searches using the program examples/enough.c found in the zlib distribution. The arguments to that program are the number of symbols, the initial root table size, and the - maximum bit length of a code. "enough 286 9 15" for literal/length codes - returns 852, and "enough 30 6 15" for distance codes returns 592. The - initial root table size (9 or 6) is found in the fifth argument of the + maximum bit length of a code. "enough 286 10 15" for literal/length codes + returns 1332, and "enough 30 9 15" for distance codes returns 592. + The initial root table size (10 or 9) is found in the fifth argument of the inflate_table() calls in inflate.c and infback.c. If the root table size is changed, then these maximum sizes would be need to be recalculated and updated. */ -#define ENOUGH_LENS 852 +#define ENOUGH_LENS 1332 #define ENOUGH_DISTS 592 #define ENOUGH (ENOUGH_LENS+ENOUGH_DISTS) @@ -60,3 +60,5 @@ typedef enum { int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, unsigned codes, code FAR * FAR *table, unsigned FAR *bits, unsigned short FAR *work); +struct inflate_state; +void ZLIB_INTERNAL inflate_fixed(struct inflate_state FAR *state); diff --git a/packages/runtime/vendor/zlib/slide_hash_simd.h b/packages/runtime/vendor/zlib/slide_hash_simd.h new file mode 100644 index 000000000..1000b774c --- /dev/null +++ b/packages/runtime/vendor/zlib/slide_hash_simd.h @@ -0,0 +1,116 @@ +/* slide_hash_simd.h + * + * Copyright 2022 The Chromium Authors + * Use of this source code is governed by a BSD-style license that can be + * found in the Chromium source repository LICENSE file. + */ + +#ifndef SLIDE_HASH_SIMD_H +#define SLIDE_HASH_SIMD_H + +#include "deflate.h" + +#ifndef INLINE +#if defined(_MSC_VER) && !defined(__clang__) +#define INLINE __inline +#else +#define INLINE inline +#endif +#endif + +#if defined(CPU_NO_SIMD) + +#error SIMD has been disabled for your build target + +#elif defined(DEFLATE_SLIDE_HASH_SSE2) + +#include /* SSE2 */ + +#define Z_SLIDE_INIT_SIMD(wsize) _mm_set1_epi16((ush)(wsize)) + +#define Z_SLIDE_HASH_SIMD(table, size, vector_wsize) \ + for (const Posf* const end = table + size; table != end;) { \ + __m128i vO = _mm_loadu_si128((__m128i *)(table + 0)); \ + vO = _mm_subs_epu16(vO, vector_wsize); \ + _mm_storeu_si128((__m128i *)(table + 0), vO); \ + table += 8; \ + } + +typedef __m128i z_vec128i_u16x8_t; + +#elif defined(DEFLATE_SLIDE_HASH_NEON) + +#include /* NEON */ + +#define Z_SLIDE_INIT_SIMD(wsize) vdupq_n_u16((ush)(wsize)) + +#define Z_SLIDE_HASH_SIMD(table, size, vector_wsize) \ + for (const Posf* const end = table + size; table != end;) { \ + uint16x8_t vO = vld1q_u16(table + 0); \ + uint16x8_t v8 = vld1q_u16(table + 8); \ + vO = vqsubq_u16(vO, vector_wsize); \ + v8 = vqsubq_u16(v8, vector_wsize); \ + vst1q_u16(table + 0, vO); \ + vst1q_u16(table + 8, v8); \ + table += 8 + 8; \ + } + +typedef uint16x8_t z_vec128i_u16x8_t; + +#else + +#error slide_hash_simd is not defined for your build target + +#endif + +/* =========================================================================== + * Slide the hash table when sliding the window down (could be avoided with 32 + * bit values at the expense of memory usage). We slide even when level == 0 to + * keep the hash table consistent if we switch back to level > 0 later. + */ +local INLINE void slide_hash_simd( + Posf *head, Posf *prev, const uInt w_size, const uInt hash_size) { + /* + * The SIMD implementation of the hash table slider assumes: + * + * 1. hash chain offset is 2 bytes. Should be true as Pos is "ush" type. + */ + Assert(sizeof(Pos) == 2, "Pos type size error: should be 2 bytes"); + Assert(sizeof(ush) == 2, "ush type size error: should be 2 bytes"); + + Assert(hash_size <= (1 << 16), "Hash table maximum size error"); + Assert(hash_size >= (1 << 8), "Hash table minimum size error"); + Assert(w_size == (ush)w_size, "Prev table size error"); + + /* + * 2. The hash & prev table sizes are a multiple of 32 bytes (256 bits), + * since the NEON table slider moves two 128-bit items per loop (loop is + * unrolled on NEON for performance, see http://crbug.com/863257). + */ + Assert(!((hash_size * sizeof(head[0])) & (32 - 1)), + "Hash table size error: should be a multiple of 32 bytes"); + Assert(!((w_size * sizeof(prev[0])) & (32 - 1)), + "Prev table size error: should be a multiple of 32 bytes"); + + /* + * Duplicate (ush)w_size in each uint16_t component of a 128-bit vector. + */ + const z_vec128i_u16x8_t vec_wsize = Z_SLIDE_INIT_SIMD(w_size); + + /* + * Slide {head,prev} hash chain values: subtracts (ush)w_size from every + * value with a saturating SIMD subtract, to clamp the result to 0(NIL), + * to implement slide_hash() `(m >= wsize ? m - wsize : NIL);` code. + */ + Z_SLIDE_HASH_SIMD(head, hash_size, vec_wsize); +#ifndef FASTEST + Z_SLIDE_HASH_SIMD(prev, w_size, vec_wsize); +#endif + +} + +#undef z_vec128i_u16x8_t +#undef Z_SLIDE_HASH_SIMD +#undef Z_SLIDE_INIT_SIMD + +#endif /* SLIDE_HASH_SIMD_H */ diff --git a/packages/runtime/vendor/zlib/trees.c b/packages/runtime/vendor/zlib/trees.c index 6a523ef34..1aa155102 100644 --- a/packages/runtime/vendor/zlib/trees.c +++ b/packages/runtime/vendor/zlib/trees.c @@ -1,5 +1,5 @@ /* trees.c -- output deflated data using Huffman coding - * Copyright (C) 1995-2024 Jean-loup Gailly + * Copyright (C) 1995-2026 Jean-loup Gailly * detect_data_type() function provided freely by Cosmin Truta, 2006 * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -112,7 +112,7 @@ local int base_dist[D_CODES]; #else # include "trees.h" -#endif /* GEN_TREES_H */ +#endif /* defined(GEN_TREES_H) || !defined(STDC) */ struct static_tree_desc_s { const ct_data *static_tree; /* static tree or NULL */ @@ -141,18 +141,25 @@ local TCONST static_tree_desc static_bl_desc = * Output a short LSB first on the stream. * IN assertion: there is enough room in pendingBuf. */ +#if defined(DEFLATE_CHUNK_WRITE_64LE) +#define put_short(s, w) { \ + uint16_t data = (uint16_t)(w); \ + zmemcpy(&s->pending_buf[s->pending], &data, sizeof(data)); \ + s->pending += 2; \ + } +#else #define put_short(s, w) { \ put_byte(s, (uch)((w) & 0xff)); \ put_byte(s, (uch)((ush)(w) >> 8)); \ } - +#endif /* DEFLATE_CHUNK_WRITE_64LE */ /* =========================================================================== * Reverse the first len bits of a code, using straightforward code (a faster * method would use a table) * IN assertion: 1 <= len <= 15 */ local unsigned bi_reverse(unsigned code, int len) { - register unsigned res = 0; + unsigned res = 0; do { res |= code & 1; code >>= 1, res <<= 1; @@ -164,6 +171,13 @@ local unsigned bi_reverse(unsigned code, int len) { * Flush the bit buffer, keeping at most 7 bits in it. */ local void bi_flush(deflate_state *s) { +#if defined(DEFLATE_CHUNK_WRITE_64LE) + while (s->bi_valid >= 16) { + put_short(s, s->bi_buf); + s->bi_buf >>= 16; + s->bi_valid -= 16; + } +#endif /* DEFLATE_CHUNK_WRITE_64LE */ if (s->bi_valid == 16) { put_short(s, s->bi_buf); s->bi_buf = 0; @@ -173,21 +187,31 @@ local void bi_flush(deflate_state *s) { s->bi_buf >>= 8; s->bi_valid -= 8; } + + Assert(s->bi_valid >= 0 && s->bi_valid <= 7, "bad bi_flush"); } /* =========================================================================== * Flush the bit buffer and align the output on a byte boundary */ local void bi_windup(deflate_state *s) { +#if defined(DEFLATE_CHUNK_WRITE_64LE) + while (s->bi_valid >= 16) { + put_short(s, s->bi_buf); + s->bi_buf >>= 16; + s->bi_valid -= 16; + } +#endif /* DEFLATE_CHUNK_WRITE_64LE */ if (s->bi_valid > 8) { put_short(s, s->bi_buf); } else if (s->bi_valid > 0) { put_byte(s, (Byte)s->bi_buf); } + s->bi_used = ((s->bi_valid - 1) & 7) + 1; s->bi_buf = 0; s->bi_valid = 0; #ifdef ZLIB_DEBUG - s->bits_sent = (s->bits_sent + 7) & ~7; + s->bits_sent = (s->bits_sent + 7) & ~(ulg)7; #endif } @@ -248,7 +272,28 @@ local void gen_trees_header(void); * Send a value on a given number of bits. * IN assertion: length <= 16 and value fits in length bits. */ +#if defined(DEFLATE_CHUNK_WRITE_64LE) +local void send_bits(deflate_state *s, uint64_t value, int length) +{ + Tracevv((stderr," l %2d v %4llx ", length, (unsigned long long)value)); + Assert(s->bi_valid >= 0 && s->bi_valid <= 63, "invalid bi_valid"); + Assert(length > 0 && length <= 15, "invalid length"); #ifdef ZLIB_DEBUG + s->bits_sent += (ulg)length; +#endif + + s->bi_buf |= (value << s->bi_valid); + s->bi_valid += length; + + if (s->bi_valid >= Buf_size) { + zmemcpy(&s->pending_buf[s->pending], &s->bi_buf, sizeof(s->bi_buf)); + s->pending += 8; + s->bi_valid -= Buf_size; + s->bi_buf = value >> (length - s->bi_valid); + } + Assert(s->bi_valid >= 0 && s->bi_valid <= 63, "invalid bi_valid"); +} +#elif defined(ZLIB_DEBUG) local void send_bits(deflate_state *s, int value, int length) { Tracevv((stderr," l %2d v %4x ", length, value)); Assert(length > 0 && length <= 15, "invalid length"); @@ -268,8 +313,7 @@ local void send_bits(deflate_state *s, int value, int length) { s->bi_valid += length; } } -#else /* !ZLIB_DEBUG */ - +#else /* !ZLIB_DEBUG && !DEFLATE_CHUNK_WRITE_64LE */ #define send_bits(s, value, length) \ { int len = length;\ if (s->bi_valid > (int)Buf_size - len) {\ @@ -283,7 +327,7 @@ local void send_bits(deflate_state *s, int value, int length) { s->bi_valid += len;\ }\ } -#endif /* ZLIB_DEBUG */ +#endif /* DEFLATE_CHUNK_WRITE_64LE */ /* the arguments must not have side effects */ @@ -466,6 +510,7 @@ void ZLIB_INTERNAL _tr_init(deflate_state *s) { s->bi_buf = 0; s->bi_valid = 0; + s->bi_used = 0; #ifdef ZLIB_DEBUG s->compressed_len = 0L; s->bits_sent = 0L; @@ -724,7 +769,7 @@ local void scan_tree(deflate_state *s, ct_data *tree, int max_code) { if (++count < max_count && curlen == nextlen) { continue; } else if (count < min_count) { - s->bl_tree[curlen].Freq += count; + s->bl_tree[curlen].Freq += (ush)count; } else if (curlen != 0) { if (curlen != prevlen) s->bl_tree[curlen].Freq++; s->bl_tree[REP_3_6].Freq++; @@ -817,7 +862,7 @@ local int build_bl_tree(deflate_state *s) { } /* Update opt_len to include the bit length tree and counts */ s->opt_len += 3*((ulg)max_blindex + 1) + 5 + 5 + 4; - Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld", + Tracev((stderr, "\ndyn trees: dyn %lu, stat %lu", s->opt_len, s->static_len)); return max_blindex; @@ -843,13 +888,13 @@ local void send_all_trees(deflate_state *s, int lcodes, int dcodes, Tracev((stderr, "\nbl code %2d ", bl_order[rank])); send_bits(s, s->bl_tree[bl_order[rank]].Len, 3); } - Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent)); + Tracev((stderr, "\nbl tree: sent %lu", s->bits_sent)); send_tree(s, (ct_data *)s->dyn_ltree, lcodes - 1); /* literal tree */ - Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent)); + Tracev((stderr, "\nlit tree: sent %lu", s->bits_sent)); send_tree(s, (ct_data *)s->dyn_dtree, dcodes - 1); /* distance tree */ - Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent)); + Tracev((stderr, "\ndist tree: sent %lu", s->bits_sent)); } /* =========================================================================== @@ -932,13 +977,14 @@ local void compress_block(deflate_state *s, const ct_data *ltree, extra = extra_dbits[code]; if (extra != 0) { dist -= (unsigned)base_dist[code]; - send_bits(s, dist, extra); /* send the extra distance bits */ + send_bits(s, (int)dist, extra); /* send the extra bits */ } } /* literal or match pair ? */ /* Check for no overlay of pending_buf on needed symbols */ #ifdef LIT_MEM - Assert(s->pending < 2 * (s->lit_bufsize + sx), "pendingBuf overflow"); + Assert(s->pending < (s->lit_bufsize << 1) + (sx << 1), + "pendingBuf overflow"); #else Assert(s->pending < s->lit_bufsize + sx, "pendingBuf overflow"); #endif @@ -1006,11 +1052,11 @@ void ZLIB_INTERNAL _tr_flush_block(deflate_state *s, charf *buf, /* Construct the literal and distance trees */ build_tree(s, (tree_desc *)(&(s->l_desc))); - Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len, + Tracev((stderr, "\nlit data: dyn %lu, stat %lu", s->opt_len, s->static_len)); build_tree(s, (tree_desc *)(&(s->d_desc))); - Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len, + Tracev((stderr, "\ndist data: dyn %lu, stat %lu", s->opt_len, s->static_len)); /* At this point, opt_len and static_len are the total bit lengths of * the compressed block data, excluding the tree representations. @@ -1083,7 +1129,7 @@ void ZLIB_INTERNAL _tr_flush_block(deflate_state *s, charf *buf, #endif } Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len >> 3, - s->compressed_len - 7*last)); + s->compressed_len - 7*(ulg)last)); } /* =========================================================================== diff --git a/packages/runtime/vendor/zlib/uncompr.c b/packages/runtime/vendor/zlib/uncompr.c index 5e256663b..2195e7855 100644 --- a/packages/runtime/vendor/zlib/uncompr.c +++ b/packages/runtime/vendor/zlib/uncompr.c @@ -1,5 +1,5 @@ /* uncompr.c -- decompress a memory buffer - * Copyright (C) 1995-2003, 2010, 2014, 2016 Jean-loup Gailly, Mark Adler + * Copyright (C) 1995-2026 Jean-loup Gailly, Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -23,24 +23,24 @@ memory, Z_BUF_ERROR if there was not enough room in the output buffer, or Z_DATA_ERROR if the input data was corrupted, including if the input data is an incomplete zlib stream. + + The _z versions of the functions take size_t length arguments. */ -int ZEXPORT uncompress2(Bytef *dest, uLongf *destLen, const Bytef *source, - uLong *sourceLen) { +int ZEXPORT uncompress2_z(Bytef *dest, z_size_t *destLen, const Bytef *source, + z_size_t *sourceLen) { z_stream stream; int err; const uInt max = (uInt)-1; - uLong len, left; - Byte buf[1]; /* for detection of incomplete stream when *destLen == 0 */ + z_size_t len, left; + + if (sourceLen == NULL || (*sourceLen > 0 && source == NULL) || + destLen == NULL || (*destLen > 0 && dest == NULL)) + return Z_STREAM_ERROR; len = *sourceLen; - if (*destLen) { - left = *destLen; - *destLen = 0; - } - else { - left = 1; - dest = buf; - } + left = *destLen; + if (left == 0 && dest == Z_NULL) + dest = (Bytef *)&stream.reserved; /* next_out cannot be NULL */ stream.next_in = (z_const Bytef *)source; stream.avail_in = 0; @@ -56,30 +56,46 @@ int ZEXPORT uncompress2(Bytef *dest, uLongf *destLen, const Bytef *source, do { if (stream.avail_out == 0) { - stream.avail_out = left > (uLong)max ? max : (uInt)left; + stream.avail_out = left > (z_size_t)max ? max : (uInt)left; left -= stream.avail_out; } if (stream.avail_in == 0) { - stream.avail_in = len > (uLong)max ? max : (uInt)len; + stream.avail_in = len > (z_size_t)max ? max : (uInt)len; len -= stream.avail_in; } err = inflate(&stream, Z_NO_FLUSH); } while (err == Z_OK); - *sourceLen -= len + stream.avail_in; - if (dest != buf) - *destLen = stream.total_out; - else if (stream.total_out && err == Z_BUF_ERROR) - left = 1; + /* Set len and left to the unused input data and unused output space. Set + *sourceLen to the amount of input consumed. Set *destLen to the amount + of data produced. */ + len += stream.avail_in; + left += stream.avail_out; + *sourceLen -= len; + *destLen -= left; inflateEnd(&stream); return err == Z_STREAM_END ? Z_OK : err == Z_NEED_DICT ? Z_DATA_ERROR : - err == Z_BUF_ERROR && left + stream.avail_out ? Z_DATA_ERROR : + err == Z_BUF_ERROR && len == 0 ? Z_DATA_ERROR : err; } - +int ZEXPORT uncompress2(Bytef *dest, uLongf *destLen, const Bytef *source, + uLong *sourceLen) { + int ret; + z_size_t got = *destLen, used = *sourceLen; + ret = uncompress2_z(dest, &got, source, &used); + *sourceLen = (uLong)used; + *destLen = (uLong)got; + return ret; +} +int ZEXPORT uncompress_z(Bytef *dest, z_size_t *destLen, const Bytef *source, + z_size_t sourceLen) { + z_size_t used = sourceLen; + return uncompress2_z(dest, destLen, source, &used); +} int ZEXPORT uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen) { - return uncompress2(dest, destLen, source, &sourceLen); + uLong used = sourceLen; + return uncompress2(dest, destLen, source, &used); } diff --git a/packages/runtime/vendor/zlib/zconf.h b/packages/runtime/vendor/zlib/zconf.h index 62adc8d84..4d64a6530 100644 --- a/packages/runtime/vendor/zlib/zconf.h +++ b/packages/runtime/vendor/zlib/zconf.h @@ -1,5 +1,5 @@ /* zconf.h -- configuration of the zlib compression library - * Copyright (C) 1995-2024 Jean-loup Gailly, Mark Adler + * Copyright (C) 1995-2026 Jean-loup Gailly, Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -8,6 +8,18 @@ #ifndef ZCONF_H #define ZCONF_H +/* + * This library is also built as a part of AOSP, which does not need to include + * chromeconf.h. This config does not want chromeconf.h, so it can set this + * macro to opt out. While this works today, there's no guarantee that building + * zlib outside of Chromium keeps working in the future. + */ +#if !defined(CHROMIUM_ZLIB_NO_CHROMECONF) +/* This include does prefixing as below, but with an updated set of names. Also + * sets up export macros in component builds. */ +//#include "chromeconf.h" +#endif + /* * If you *really* need a unique prefix for all types and library functions, * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it. @@ -33,7 +45,10 @@ # ifndef Z_SOLO # define compress z_compress # define compress2 z_compress2 +# define compress_z z_compress_z +# define compress2_z z_compress2_z # define compressBound z_compressBound +# define compressBound_z z_compressBound_z # endif # define crc32 z_crc32 # define crc32_combine z_crc32_combine @@ -44,6 +59,7 @@ # define crc32_z z_crc32_z # define deflate z_deflate # define deflateBound z_deflateBound +# define deflateBound_z z_deflateBound_z # define deflateCopy z_deflateCopy # define deflateEnd z_deflateEnd # define deflateGetDictionary z_deflateGetDictionary @@ -59,6 +75,7 @@ # define deflateSetDictionary z_deflateSetDictionary # define deflateSetHeader z_deflateSetHeader # define deflateTune z_deflateTune +# define deflateUsed z_deflateUsed # define deflate_copyright z_deflate_copyright # define get_crc_table z_get_crc_table # ifndef Z_SOLO @@ -128,9 +145,12 @@ # define inflate_copyright z_inflate_copyright # define inflate_fast z_inflate_fast # define inflate_table z_inflate_table +# define inflate_fixed z_inflate_fixed # ifndef Z_SOLO # define uncompress z_uncompress # define uncompress2 z_uncompress2 +# define uncompress_z z_uncompress_z +# define uncompress2_z z_uncompress2_z # endif # define zError z_zError # ifndef Z_SOLO @@ -234,10 +254,12 @@ # endif #endif -#if defined(ZLIB_CONST) && !defined(z_const) -# define z_const const -#else -# define z_const +#ifndef z_const +# ifdef ZLIB_CONST +# define z_const const +# else +# define z_const +# endif #endif #ifdef Z_SOLO @@ -388,6 +410,9 @@ #ifndef FAR # define FAR #endif +#ifndef far +# define far +#endif #if !defined(__MACTYPES__) typedef unsigned char Byte; /* 8 bits */ @@ -433,11 +458,11 @@ typedef uLong FAR uLongf; typedef unsigned long z_crc_t; #endif -#ifdef HAVE_UNISTD_H /* may be set to #if 1 by ./configure */ +#if !defined(_WIN32) # define Z_HAVE_UNISTD_H #endif -#ifdef HAVE_STDARG_H /* may be set to #if 1 by ./configure */ +#if defined(HAVE_STDARG_H) && HAVE_STDARG_H-0 # define Z_HAVE_STDARG_H #endif @@ -470,12 +495,8 @@ typedef uLong FAR uLongf; #endif #ifndef Z_HAVE_UNISTD_H -# ifdef __WATCOMC__ -# define Z_HAVE_UNISTD_H -# endif -#endif -#ifndef Z_HAVE_UNISTD_H -# if defined(_LARGEFILE64_SOURCE) && !defined(_WIN32) +# if defined(__WATCOMC__) || defined(__GO32__) || \ + (defined(_LARGEFILE64_SOURCE) && !defined(_WIN32)) # define Z_HAVE_UNISTD_H # endif #endif @@ -510,17 +531,19 @@ typedef uLong FAR uLongf; #endif #ifndef z_off_t -# define z_off_t long +# define z_off_t long long #endif #if !defined(_WIN32) && defined(Z_LARGE64) # define z_off64_t off64_t +#elif defined(__MINGW32__) +# define z_off64_t long long +#elif defined(_WIN32) && !defined(__GNUC__) +# define z_off64_t __int64 +#elif defined(__GO32__) +# define z_off64_t offset_t #else -# if defined(_WIN32) && !defined(__GNUC__) -# define z_off64_t __int64 -# else -# define z_off64_t z_off_t -# endif +# define z_off64_t z_off_t #endif /* MVS linker does not support external names larger than 8 bytes */ diff --git a/packages/runtime/vendor/zlib/zlib.h b/packages/runtime/vendor/zlib/zlib.h index 8d4b932ea..13909f2f8 100644 --- a/packages/runtime/vendor/zlib/zlib.h +++ b/packages/runtime/vendor/zlib/zlib.h @@ -1,7 +1,7 @@ /* zlib.h -- interface of the 'zlib' general purpose compression library - version 1.3.1, January 22nd, 2024 + version 1.3.2.1, February xxth, 2026 - Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler + Copyright (C) 1995-2026 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages @@ -24,25 +24,29 @@ The data format used by the zlib library is described by RFCs (Request for - Comments) 1950 to 1952 in the files http://tools.ietf.org/html/rfc1950 + Comments) 1950 to 1952 at https://datatracker.ietf.org/doc/html/rfc1950 (zlib format), rfc1951 (deflate format) and rfc1952 (gzip format). */ #ifndef ZLIB_H #define ZLIB_H -#include "zconf.h" +#ifdef ZLIB_BUILD +# include +#else +# include "zconf.h" +#endif #ifdef __cplusplus extern "C" { #endif -#define ZLIB_VERSION "1.3.1" -#define ZLIB_VERNUM 0x1310 +#define ZLIB_VERSION "1.3.2.1-motley" +#define ZLIB_VERNUM 0x1321 #define ZLIB_VER_MAJOR 1 #define ZLIB_VER_MINOR 3 -#define ZLIB_VER_REVISION 1 -#define ZLIB_VER_SUBREVISION 0 +#define ZLIB_VER_REVISION 2 +#define ZLIB_VER_SUBREVISION 1 /* The 'zlib' compression library provides in-memory compression and @@ -359,7 +363,6 @@ ZEXTERN int ZEXPORT deflate(z_streamp strm, int flush); continue compressing. */ - ZEXTERN int ZEXPORT deflateEnd(z_streamp strm); /* All dynamically allocated data structures for this stream are freed. @@ -441,7 +444,7 @@ ZEXTERN int ZEXPORT inflate(z_streamp strm, int flush); The Z_BLOCK option assists in appending to or combining deflate streams. To assist in this, on return inflate() always sets strm->data_type to the - number of unused bits in the last byte taken from strm->next_in, plus 64 if + number of unused bits in the input taken from strm->next_in, plus 64 if inflate() is currently decoding the last block in the deflate stream, plus 128 if inflate() returned immediately after decoding an end-of-block code or decoding the complete header up to just before the first byte of the deflate @@ -587,18 +590,21 @@ ZEXTERN int ZEXPORT deflateInit2(z_streamp strm, The strategy parameter is used to tune the compression algorithm. Use the value Z_DEFAULT_STRATEGY for normal data, Z_FILTERED for data produced by a - filter (or predictor), Z_HUFFMAN_ONLY to force Huffman encoding only (no - string match), or Z_RLE to limit match distances to one (run-length - encoding). Filtered data consists mostly of small values with a somewhat - random distribution. In this case, the compression algorithm is tuned to - compress them better. The effect of Z_FILTERED is to force more Huffman - coding and less string matching; it is somewhat intermediate between - Z_DEFAULT_STRATEGY and Z_HUFFMAN_ONLY. Z_RLE is designed to be almost as - fast as Z_HUFFMAN_ONLY, but give better compression for PNG image data. The - strategy parameter only affects the compression ratio but not the - correctness of the compressed output even if it is not set appropriately. - Z_FIXED prevents the use of dynamic Huffman codes, allowing for a simpler - decoder for special applications. + filter (or predictor), Z_RLE to limit match distances to one (run-length + encoding), or Z_HUFFMAN_ONLY to force Huffman encoding only (no string + matching). Filtered data consists mostly of small values with a somewhat + random distribution, as produced by the PNG filters. In this case, the + compression algorithm is tuned to compress them better. The effect of + Z_FILTERED is to force more Huffman coding and less string matching than the + default; it is intermediate between Z_DEFAULT_STRATEGY and Z_HUFFMAN_ONLY. + Z_RLE is almost as fast as Z_HUFFMAN_ONLY, but should give better + compression for PNG image data than Huffman only. The degree of string + matching from most to none is: Z_DEFAULT_STRATEGY, Z_FILTERED, Z_RLE, then + Z_HUFFMAN_ONLY. The strategy parameter affects the compression ratio but + never the correctness of the compressed output, even if it is not set + optimally for the given data. Z_FIXED uses the default string matching, but + prevents the use of dynamic Huffman codes, allowing for a simpler decoder + for special applications. deflateInit2 returns Z_OK if success, Z_MEM_ERROR if there was not enough memory, Z_STREAM_ERROR if any parameter is invalid (such as an invalid @@ -758,8 +764,8 @@ ZEXTERN int ZEXPORT deflateTune(z_streamp strm, returns Z_OK on success, or Z_STREAM_ERROR for an invalid deflate stream. */ -ZEXTERN uLong ZEXPORT deflateBound(z_streamp strm, - uLong sourceLen); +ZEXTERN uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen); +ZEXTERN z_size_t ZEXPORT deflateBound_z(z_streamp strm, z_size_t sourceLen); /* deflateBound() returns an upper bound on the compressed size after deflation of sourceLen bytes. It must be called after deflateInit() or @@ -771,6 +777,9 @@ ZEXTERN uLong ZEXPORT deflateBound(z_streamp strm, to return Z_STREAM_END. Note that it is possible for the compressed size to be larger than the value returned by deflateBound() if flush options other than Z_FINISH or Z_NO_FLUSH are used. + + delfateBound_z() is the same, but takes and returns a size_t length. Note + that a long is 32 bits on Windows. */ ZEXTERN int ZEXPORT deflatePending(z_streamp strm, @@ -785,6 +794,21 @@ ZEXTERN int ZEXPORT deflatePending(z_streamp strm, or bits are Z_NULL, then those values are not set. deflatePending returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent. If an int is 16 bits and memLevel is 9, then + it is possible for the number of pending bytes to not fit in an unsigned. In + that case Z_BUF_ERROR is returned and *pending is set to the maximum value + of an unsigned. + */ + +ZEXTERN int ZEXPORT deflateUsed(z_streamp strm, + int *bits); +/* + deflateUsed() returns in *bits the most recent number of deflate bits used + in the last byte when flushing to a byte boundary. The result is in 1..8, or + 0 if there has not yet been a flush. This helps determine the location of + the last bit of a deflate stream. + + deflateUsed returns Z_OK if success, or Z_STREAM_ERROR if the source stream state was inconsistent. */ @@ -987,13 +1011,15 @@ ZEXTERN int ZEXPORT inflatePrime(z_streamp strm, int bits, int value); /* - This function inserts bits in the inflate input stream. The intent is - that this function is used to start inflating at a bit position in the - middle of a byte. The provided bits will be used before any bytes are used - from next_in. This function should only be used with raw inflate, and - should be used before the first inflate() call after inflateInit2() or - inflateReset(). bits must be less than or equal to 16, and that many of the - least significant bits of value will be inserted in the input. + This function inserts bits in the inflate input stream. The intent is to + use inflatePrime() to start inflating at a bit position in the middle of a + byte. The provided bits will be used before any bytes are used from + next_in. This function should be used with raw inflate, before the first + inflate() call, after inflateInit2() or inflateReset(). It can also be used + after an inflate() return indicates the end of a deflate block or header + when using Z_BLOCK. bits must be less than or equal to 16, and that many of + the least significant bits of value will be inserted in the input. The + other bits in value can be non-zero, and will be ignored. If bits is negative, then the input stream bit buffer is emptied. Then inflatePrime() can be called again to put bits in the buffer. This is used @@ -1001,7 +1027,15 @@ ZEXTERN int ZEXPORT inflatePrime(z_streamp strm, to feeding inflate codes. inflatePrime returns Z_OK if success, or Z_STREAM_ERROR if the source - stream state was inconsistent. + stream state was inconsistent, or if bits is out of range. If inflate was + in the middle of processing a header, trailer, or stored block lengths, then + it is possible for there to be only eight bits available in the bit buffer. + In that case, bits > 8 is considered out of range. However, when used as + outlined above, there will always be 16 bits available in the buffer for + insertion. As noted in its documentation above, inflate records the number + of bits in the bit buffer on return in data_type. 32 minus that is the + number of bits available for insertion. inflatePrime does not update + data_type with the new number of bits in buffer. */ ZEXTERN long ZEXPORT inflateMark(z_streamp strm); @@ -1047,20 +1081,22 @@ ZEXTERN int ZEXPORT inflateGetHeader(z_streamp strm, The text, time, xflags, and os fields are filled in with the gzip header contents. hcrc is set to true if there is a header CRC. (The header CRC - was valid if done is set to one.) If extra is not Z_NULL, then extra_max - contains the maximum number of bytes to write to extra. Once done is true, - extra_len contains the actual extra field length, and extra contains the - extra field, or that field truncated if extra_max is less than extra_len. - If name is not Z_NULL, then up to name_max characters are written there, - terminated with a zero unless the length is greater than name_max. If - comment is not Z_NULL, then up to comm_max characters are written there, - terminated with a zero unless the length is greater than comm_max. When any - of extra, name, or comment are not Z_NULL and the respective field is not - present in the header, then that field is set to Z_NULL to signal its - absence. This allows the use of deflateSetHeader() with the returned - structure to duplicate the header. However if those fields are set to - allocated memory, then the application will need to save those pointers - elsewhere so that they can be eventually freed. + was valid if done is set to one.) The extra, name, and comment pointers + much each be either Z_NULL or point to space to store that information from + the header. If extra is not Z_NULL, then extra_max contains the maximum + number of bytes that can be written to extra. Once done is true, extra_len + contains the actual extra field length, and extra contains the extra field, + or that field truncated if extra_max is less than extra_len. If name is not + Z_NULL, then up to name_max characters, including the terminating zero, are + written there. If comment is not Z_NULL, then up to comm_max characters, + including the terminating zero, are written there. The application can tell + that the name or comment did not fit in the provided space by the absence of + a terminating zero. If any of extra, name, or comment are not present in + the header, then that field's pointer is set to Z_NULL. This allows the use + of deflateSetHeader() with the returned structure to duplicate the header. + Note that if those fields initially pointed to allocated memory, then the + application will need to save them elsewhere so that they can be eventually + freed. If inflateGetHeader is not used, then the header information is simply discarded. The header is always checked for validity, including the header @@ -1208,13 +1244,14 @@ ZEXTERN uLong ZEXPORT zlibCompileFlags(void); 21: FASTEST -- deflate algorithm with only one, lowest compression level 22,23: 0 (reserved) - The sprintf variant used by gzprintf (zero is best): + The sprintf variant used by gzprintf (all zeros is best): 24: 0 = vs*, 1 = s* -- 1 means limited to 20 arguments after the format - 25: 0 = *nprintf, 1 = *printf -- 1 means gzprintf() not secure! + 25: 0 = *nprintf, 1 = *printf -- 1 means gzprintf() is not secure! 26: 0 = returns value, 1 = void -- 1 means inferred string length returned + 27: 0 = gzprintf() present, 1 = not -- 1 means gzprintf() returns an error Remainder: - 27-31: 0 (reserved) + 28-31: 0 (reserved) */ #ifndef Z_SOLO @@ -1226,11 +1263,14 @@ ZEXTERN uLong ZEXPORT zlibCompileFlags(void); stream-oriented functions. To simplify the interface, some default options are assumed (compression level and memory usage, standard memory allocation functions). The source code of these utility functions can be modified if - you need special options. + you need special options. The _z versions of the functions use the size_t + type for lengths. Note that a long is 32 bits on Windows. */ -ZEXTERN int ZEXPORT compress(Bytef *dest, uLongf *destLen, +ZEXTERN int ZEXPORT compress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen); +ZEXTERN int ZEXPORT compress_z(Bytef *dest, z_size_t *destLen, + const Bytef *source, z_size_t sourceLen); /* Compresses the source buffer into the destination buffer. sourceLen is the byte length of the source buffer. Upon entry, destLen is the total size @@ -1244,9 +1284,12 @@ ZEXTERN int ZEXPORT compress(Bytef *dest, uLongf *destLen, buffer. */ -ZEXTERN int ZEXPORT compress2(Bytef *dest, uLongf *destLen, +ZEXTERN int ZEXPORT compress2(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen, int level); +ZEXTERN int ZEXPORT compress2_z(Bytef *dest, z_size_t *destLen, + const Bytef *source, z_size_t sourceLen, + int level); /* Compresses the source buffer into the destination buffer. The level parameter has the same meaning as in deflateInit. sourceLen is the byte @@ -1261,21 +1304,24 @@ ZEXTERN int ZEXPORT compress2(Bytef *dest, uLongf *destLen, */ ZEXTERN uLong ZEXPORT compressBound(uLong sourceLen); +ZEXTERN z_size_t ZEXPORT compressBound_z(z_size_t sourceLen); /* compressBound() returns an upper bound on the compressed size after compress() or compress2() on sourceLen bytes. It would be used before a compress() or compress2() call to allocate the destination buffer. */ -ZEXTERN int ZEXPORT uncompress(Bytef *dest, uLongf *destLen, +ZEXTERN int ZEXPORT uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen); +ZEXTERN int ZEXPORT uncompress_z(Bytef *dest, z_size_t *destLen, + const Bytef *source, z_size_t sourceLen); /* Decompresses the source buffer into the destination buffer. sourceLen is - the byte length of the source buffer. Upon entry, destLen is the total size + the byte length of the source buffer. On entry, *destLen is the total size of the destination buffer, which must be large enough to hold the entire uncompressed data. (The size of the uncompressed data must have been saved previously by the compressor and transmitted to the decompressor by some - mechanism outside the scope of this compression library.) Upon exit, destLen + mechanism outside the scope of this compression library.) On exit, *destLen is the actual size of the uncompressed data. uncompress returns Z_OK if success, Z_MEM_ERROR if there was not @@ -1285,8 +1331,10 @@ ZEXTERN int ZEXPORT uncompress(Bytef *dest, uLongf *destLen, buffer with the uncompressed data up to that point. */ -ZEXTERN int ZEXPORT uncompress2(Bytef *dest, uLongf *destLen, +ZEXTERN int ZEXPORT uncompress2(Bytef *dest, uLongf *destLen, const Bytef *source, uLong *sourceLen); +ZEXTERN int ZEXPORT uncompress2_z(Bytef *dest, z_size_t *destLen, + const Bytef *source, z_size_t *sourceLen); /* Same as uncompress, except that sourceLen is a pointer, where the length of the source is *sourceLen. On return, *sourceLen is the number of @@ -1314,13 +1362,17 @@ ZEXTERN gzFile ZEXPORT gzopen(const char *path, const char *mode); 'R' for run-length encoding as in "wb1R", or 'F' for fixed code compression as in "wb9F". (See the description of deflateInit2 for more information about the strategy parameter.) 'T' will request transparent writing or - appending with no compression and not using the gzip format. + appending with no compression and not using the gzip format. 'T' cannot be + used to force transparent reading. Transparent reading is automatically + performed if there is no gzip header at the start. Transparent reading can + be disabled with the 'G' option, which will instead return an error if there + is no gzip header. 'N' will open the file in non-blocking mode. - "a" can be used instead of "w" to request that the gzip stream that will - be written be appended to the file. "+" will result in an error, since + 'a' can be used instead of 'w' to request that the gzip stream that will + be written be appended to the file. '+' will result in an error, since reading and writing to the same gzip file is not supported. The addition of - "x" when writing will create the file exclusively, which fails if the file - already exists. On systems that support it, the addition of "e" when + 'x' when writing will create the file exclusively, which fails if the file + already exists. On systems that support it, the addition of 'e' when reading or writing will set the flag to close the file on an execve() call. These functions, as well as gzip, will read and decode a sequence of gzip @@ -1339,14 +1391,22 @@ ZEXTERN gzFile ZEXPORT gzopen(const char *path, const char *mode); insufficient memory to allocate the gzFile state, or if an invalid mode was specified (an 'r', 'w', or 'a' was not provided, or '+' was provided). errno can be checked to determine if the reason gzopen failed was that the - file could not be opened. + file could not be opened. Note that if 'N' is in mode for non-blocking, the + open() itself can fail in order to not block. In that case gzopen() will + return NULL and errno will be EAGAIN or ENONBLOCK. The call to gzopen() can + then be re-tried. If the application would like to block on opening the + file, then it can use open() without O_NONBLOCK, and then gzdopen() with the + resulting file descriptor and 'N' in the mode, which will set it to non- + blocking. */ ZEXTERN gzFile ZEXPORT gzdopen(int fd, const char *mode); /* Associate a gzFile with the file descriptor fd. File descriptors are obtained from calls like open, dup, creat, pipe or fileno (if the file has - been previously opened with fopen). The mode parameter is as in gzopen. + been previously opened with fopen). The mode parameter is as in gzopen. An + 'e' in mode will set fd's flag to close the file on an execve() call. An 'N' + in mode will set fd's non-blocking flag. The next call of gzclose on the returned gzFile will also close the file descriptor fd, just like fclose(fdopen(fd, mode)) closes the file descriptor @@ -1416,10 +1476,16 @@ ZEXTERN int ZEXPORT gzread(gzFile file, voidp buf, unsigned len); stream. Alternatively, gzerror can be used before gzclose to detect this case. + gzread can be used to read a gzip file on a non-blocking device. If the + input stalls and there is no uncompressed data to return, then gzread() will + return -1, and errno will be EAGAIN or EWOULDBLOCK. gzread() can then be + called again. + gzread returns the number of uncompressed bytes actually read, less than len for end of file, or -1 for error. If len is too large to fit in an int, then nothing is read, -1 is returned, and the error state is set to - Z_STREAM_ERROR. + Z_STREAM_ERROR. If some data was read before an error, then that data is + returned until exhausted, after which the next call will signal the error. */ ZEXTERN z_size_t ZEXPORT gzfread(voidp buf, z_size_t size, z_size_t nitems, @@ -1443,15 +1509,20 @@ ZEXTERN z_size_t ZEXPORT gzfread(voidp buf, z_size_t size, z_size_t nitems, multiple of size, then the final partial item is nevertheless read into buf and the end-of-file flag is set. The length of the partial item read is not provided, but could be inferred from the result of gztell(). This behavior - is the same as the behavior of fread() implementations in common libraries, - but it prevents the direct use of gzfread() to read a concurrently written - file, resetting and retrying on end-of-file, when size is not 1. + is the same as that of fread() implementations in common libraries. This + could result in data loss if used with size != 1 when reading a concurrently + written file or a non-blocking file. In that case, use size == 1 or gzread() + instead. */ ZEXTERN int ZEXPORT gzwrite(gzFile file, voidpc buf, unsigned len); /* Compress and write the len uncompressed bytes at buf to file. gzwrite - returns the number of uncompressed bytes written or 0 in case of error. + returns the number of uncompressed bytes written, or 0 in case of error or + if len is 0. If the write destination is non-blocking, then gzwrite() may + return a number of bytes written that is not 0 and less than len. + + If len does not fit in an int, then 0 is returned and nothing is written. */ ZEXTERN z_size_t ZEXPORT gzfwrite(voidpc buf, z_size_t size, @@ -1466,9 +1537,18 @@ ZEXTERN z_size_t ZEXPORT gzfwrite(voidpc buf, z_size_t size, if there was an error. If the multiplication of size and nitems overflows, i.e. the product does not fit in a z_size_t, then nothing is written, zero is returned, and the error state is set to Z_STREAM_ERROR. + + If writing a concurrently read file or a non-blocking file with size != 1, + a partial item could be written, with no way of knowing how much of it was + not written, resulting in data loss. In that case, use size == 1 or + gzwrite() instead. */ +#if defined(STDC) || defined(Z_HAVE_STDARG_H) ZEXTERN int ZEXPORTVA gzprintf(gzFile file, const char *format, ...); +#else +ZEXTERN int ZEXPORTVA gzprintf(); +#endif /* Convert, format, compress, and write the arguments (...) to file under control of the string format, as in fprintf. gzprintf returns the number of @@ -1476,11 +1556,19 @@ ZEXTERN int ZEXPORTVA gzprintf(gzFile file, const char *format, ...); of error. The number of uncompressed bytes written is limited to 8191, or one less than the buffer size given to gzbuffer(). The caller should assure that this limit is not exceeded. If it is exceeded, then gzprintf() will - return an error (0) with nothing written. In this case, there may also be a - buffer overflow with unpredictable consequences, which is possible only if - zlib was compiled with the insecure functions sprintf() or vsprintf(), - because the secure snprintf() or vsnprintf() functions were not available. - This can be determined using zlibCompileFlags(). + return an error (0) with nothing written. + + In that last case, there may also be a buffer overflow with unpredictable + consequences, which is possible only if zlib was compiled with the insecure + functions sprintf() or vsprintf(), because the secure snprintf() and + vsnprintf() functions were not available. That would only be the case for + a non-ANSI C compiler. zlib may have been built without gzprintf() because + secure functions were not available and having gzprintf() be insecure was + not an option, in which case, gzprintf() returns Z_STREAM_ERROR. All of + these possibilities can be determined using zlibCompileFlags(). + + If a Z_BUF_ERROR is returned, then nothing was written due to a stall on + the non-blocking write destination. */ ZEXTERN int ZEXPORT gzputs(gzFile file, const char *s); @@ -1489,6 +1577,11 @@ ZEXTERN int ZEXPORT gzputs(gzFile file, const char *s); the terminating null character. gzputs returns the number of characters written, or -1 in case of error. + The number of characters written may be less than the length of the string + if the write destination is non-blocking. + + If the length of the string does not fit in an int, then -1 is returned + and nothing is written. */ ZEXTERN char * ZEXPORT gzgets(gzFile file, char *buf, int len); @@ -1501,8 +1594,13 @@ ZEXTERN char * ZEXPORT gzgets(gzFile file, char *buf, int len); left untouched. gzgets returns buf which is a null-terminated string, or it returns NULL - for end-of-file or in case of error. If there was an error, the contents at - buf are indeterminate. + for end-of-file or in case of error. If some data was read before an error, + then that data is returned until exhausted, after which the next call will + return NULL to signal the error. + + gzgets can be used on a file being concurrently written, and on a non- + blocking device, both as for gzread(). However lines may be broken in the + middle, leaving it up to the application to reassemble them as needed. */ ZEXTERN int ZEXPORT gzputc(gzFile file, int c); @@ -1513,11 +1611,19 @@ ZEXTERN int ZEXPORT gzputc(gzFile file, int c); ZEXTERN int ZEXPORT gzgetc(gzFile file); /* - Read and decompress one byte from file. gzgetc returns this byte or -1 - in case of end of file or error. This is implemented as a macro for speed. - As such, it does not do all of the checking the other functions do. I.e. - it does not check to see if file is NULL, nor whether the structure file - points to has been clobbered or not. + Read and decompress one byte from file. gzgetc returns this byte or -1 in + case of end of file or error. If some data was read before an error, then + that data is returned until exhausted, after which the next call will return + -1 to signal the error. + + This is implemented as a macro for speed. As such, it does not do all of + the checking the other functions do. I.e. it does not check to see if file + is NULL, nor whether the structure file points to has been clobbered or not. + + gzgetc can be used to read a gzip file on a non-blocking device. If the + input stalls and there is no uncompressed data to return, then gzgetc() will + return -1, and errno will be EAGAIN or EWOULDBLOCK. gzread() can then be + called again. */ ZEXTERN int ZEXPORT gzungetc(int c, gzFile file); @@ -1530,6 +1636,11 @@ ZEXTERN int ZEXPORT gzungetc(int c, gzFile file); output buffer size of pushed characters is allowed. (See gzbuffer above.) The pushed character will be discarded if the stream is repositioned with gzseek() or gzrewind(). + + gzungetc(-1, file) will force any pending seek to execute. Then gztell() + will report the position, even if the requested seek reached end of file. + This can be used to determine the number of uncompressed bytes in a gzip + file without having to read it into a buffer. */ ZEXTERN int ZEXPORT gzflush(gzFile file, int flush); @@ -1559,7 +1670,8 @@ ZEXTERN z_off_t ZEXPORT gzseek(gzFile file, If the file is opened for reading, this function is emulated but can be extremely slow. If the file is opened for writing, only forward seeks are supported; gzseek then compresses a sequence of zeroes up to the new - starting position. + starting position. For reading or writing, any actual seeking is deferred + until the next read or write operation, or close operation when writing. gzseek returns the resulting offset location as measured in bytes from the beginning of the uncompressed stream, or -1 in case of error, in @@ -1567,7 +1679,7 @@ ZEXTERN z_off_t ZEXPORT gzseek(gzFile file, would be before the current position. */ -ZEXTERN int ZEXPORT gzrewind(gzFile file); +ZEXTERN int ZEXPORT gzrewind(gzFile file); /* Rewind file. This function is supported only for reading. @@ -1575,7 +1687,7 @@ ZEXTERN int ZEXPORT gzrewind(gzFile file); */ /* -ZEXTERN z_off_t ZEXPORT gztell(gzFile file); +ZEXTERN z_off_t ZEXPORT gztell(gzFile file); Return the starting position for the next gzread or gzwrite on file. This position represents a number of bytes in the uncompressed data stream, @@ -1620,8 +1732,11 @@ ZEXTERN int ZEXPORT gzdirect(gzFile file); If gzdirect() is used immediately after gzopen() or gzdopen() it will cause buffers to be allocated to allow reading the file to determine if it - is a gzip file. Therefore if gzbuffer() is used, it should be called before - gzdirect(). + is a gzip file. Therefore if gzbuffer() is used, it should be called before + gzdirect(). If the input is being written concurrently or the device is non- + blocking, then gzdirect() may give a different answer once four bytes of + input have been accumulated, which is what is needed to confirm or deny a + gzip header. Before this, gzdirect() will return true (1). When writing, gzdirect() returns true (1) if transparent writing was requested ("wT" for the gzopen() mode), or false (0) otherwise. (Note: @@ -1631,7 +1746,7 @@ ZEXTERN int ZEXPORT gzdirect(gzFile file); gzip file reading and decompression, which may not be desired.) */ -ZEXTERN int ZEXPORT gzclose(gzFile file); +ZEXTERN int ZEXPORT gzclose(gzFile file); /* Flush all pending output for file, if necessary, close file and deallocate the (de)compression state. Note that once file is closed, you @@ -1659,9 +1774,10 @@ ZEXTERN int ZEXPORT gzclose_w(gzFile file); ZEXTERN const char * ZEXPORT gzerror(gzFile file, int *errnum); /* Return the error message for the last error which occurred on file. - errnum is set to zlib error number. If an error occurred in the file system - and not in the compression library, errnum is set to Z_ERRNO and the - application may consult errno to get the exact error code. + If errnum is not NULL, *errnum is set to zlib error number. If an error + occurred in the file system and not in the compression library, *errnum is + set to Z_ERRNO and the application may consult errno to get the exact error + code. The application must not modify the returned string. Future calls to this function may invalidate the previously returned string. If file is @@ -1712,7 +1828,8 @@ ZEXTERN uLong ZEXPORT adler32(uLong adler, const Bytef *buf, uInt len); ZEXTERN uLong ZEXPORT adler32_z(uLong adler, const Bytef *buf, z_size_t len); /* - Same as adler32(), but with a size_t length. + Same as adler32(), but with a size_t length. Note that a long is 32 bits + on Windows. */ /* @@ -1748,7 +1865,8 @@ ZEXTERN uLong ZEXPORT crc32(uLong crc, const Bytef *buf, uInt len); ZEXTERN uLong ZEXPORT crc32_z(uLong crc, const Bytef *buf, z_size_t len); /* - Same as crc32(), but with a size_t length. + Same as crc32(), but with a size_t length. Note that a long is 32 bits on + Windows. */ /* @@ -1758,14 +1876,14 @@ ZEXTERN uLong ZEXPORT crc32_combine(uLong crc1, uLong crc2, z_off_t len2); seq1 and seq2 with lengths len1 and len2, CRC-32 check values were calculated for each, crc1 and crc2. crc32_combine() returns the CRC-32 check value of seq1 and seq2 concatenated, requiring only crc1, crc2, and - len2. len2 must be non-negative. + len2. len2 must be non-negative, otherwise zero is returned. */ /* ZEXTERN uLong ZEXPORT crc32_combine_gen(z_off_t len2); Return the operator corresponding to length len2, to be used with - crc32_combine_op(). len2 must be non-negative. + crc32_combine_op(). len2 must be non-negative, otherwise zero is returned. */ ZEXTERN uLong ZEXPORT crc32_combine_op(uLong crc1, uLong crc2, uLong op); @@ -1844,6 +1962,11 @@ ZEXTERN int ZEXPORT gzgetc_(gzFile file); /* backward compatibility */ # undef z_gzgetc # define z_gzgetc(g) \ ((g)->have ? ((g)->have--, (g)->pos++, *((g)->next)++) : (gzgetc)(g)) +#elif defined(Z_CR_PREFIX_SET) +# undef gzgetc +# define gzgetc(g) \ + ((g)->have ? ((g)->have--, (g)->pos++, *((g)->next)++) \ + : (Cr_z_gzgetc)(g)) #else # define gzgetc(g) \ ((g)->have ? ((g)->have--, (g)->pos++, *((g)->next)++) : (gzgetc)(g)) @@ -1875,22 +1998,47 @@ ZEXTERN int ZEXPORT gzgetc_(gzFile file); /* backward compatibility */ # define z_crc32_combine z_crc32_combine64 # define z_crc32_combine_gen z_crc32_combine_gen64 # else +# ifdef gzopen +# undef gzopen +# endif # define gzopen gzopen64 +# ifdef gzseek +# undef gzseek +# endif # define gzseek gzseek64 +# ifdef gztell +# undef gztell +# endif # define gztell gztell64 +# ifdef gzoffset +# undef gzoffset +# endif # define gzoffset gzoffset64 +# ifdef adler32_combine +# undef adler32_combine +# endif # define adler32_combine adler32_combine64 -# define crc32_combine crc32_combine64 -# define crc32_combine_gen crc32_combine_gen64 +# ifdef crc32_combine +# undef crc32_combine +# endif +# ifdef crc32_combine64 +# undef crc32_combine64 +# endif +# ifdef crc32_combine_gen +# undef crc32_combine_gen +# endif +# ifdef crc32_combine_op +# undef crc32_combine_op +# endif # endif # ifndef Z_LARGE64 ZEXTERN gzFile ZEXPORT gzopen64(const char *, const char *); ZEXTERN z_off_t ZEXPORT gzseek64(gzFile, z_off_t, int); ZEXTERN z_off_t ZEXPORT gztell64(gzFile); ZEXTERN z_off_t ZEXPORT gzoffset64(gzFile); - ZEXTERN uLong ZEXPORT adler32_combine64(uLong, uLong, z_off_t); - ZEXTERN uLong ZEXPORT crc32_combine64(uLong, uLong, z_off_t); - ZEXTERN uLong ZEXPORT crc32_combine_gen64(z_off_t); + ZEXTERN uLong ZEXPORT adler32_combine64(uLong, uLong, z_off64_t); + ZEXTERN uLong ZEXPORT crc32_combine64(uLong, uLong, z_off64_t); + ZEXTERN uLong ZEXPORT crc32_combine_gen64(z_off64_t); # endif #else ZEXTERN gzFile ZEXPORT gzopen(const char *, const char *); diff --git a/packages/runtime/vendor/zlib/zutil.c b/packages/runtime/vendor/zlib/zutil.c index b1c5d2d3c..0e30c5664 100644 --- a/packages/runtime/vendor/zlib/zutil.c +++ b/packages/runtime/vendor/zlib/zutil.c @@ -1,5 +1,5 @@ /* zutil.c -- target dependent utility functions for the compression library - * Copyright (C) 1995-2017 Jean-loup Gailly + * Copyright (C) 1995-2026 Jean-loup Gailly * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -86,28 +86,36 @@ uLong ZEXPORT zlibCompileFlags(void) { flags += 1L << 21; #endif #if defined(STDC) || defined(Z_HAVE_STDARG_H) -# ifdef NO_vsnprintf - flags += 1L << 25; -# ifdef HAS_vsprintf_void - flags += 1L << 26; -# endif -# else -# ifdef HAS_vsnprintf_void - flags += 1L << 26; -# endif -# endif +# ifdef NO_vsnprintf +# ifdef ZLIB_INSECURE + flags += 1L << 25; +# else + flags += 1L << 27; +# endif +# ifdef HAS_vsprintf_void + flags += 1L << 26; +# endif +# else +# ifdef HAS_vsnprintf_void + flags += 1L << 26; +# endif +# endif #else flags += 1L << 24; -# ifdef NO_snprintf - flags += 1L << 25; -# ifdef HAS_sprintf_void - flags += 1L << 26; -# endif -# else -# ifdef HAS_snprintf_void - flags += 1L << 26; -# endif -# endif +# ifdef NO_snprintf +# ifdef ZLIB_INSECURE + flags += 1L << 25; +# else + flags += 1L << 27; +# endif +# ifdef HAS_sprintf_void + flags += 1L << 26; +# endif +# else +# ifdef HAS_snprintf_void + flags += 1L << 26; +# endif +# endif #endif return flags; } @@ -142,28 +150,33 @@ const char * ZEXPORT zError(int err) { #ifndef HAVE_MEMCPY -void ZLIB_INTERNAL zmemcpy(Bytef* dest, const Bytef* source, uInt len) { - if (len == 0) return; - do { - *dest++ = *source++; /* ??? to be unrolled */ - } while (--len != 0); +void ZLIB_INTERNAL zmemcpy(void FAR *dst, const void FAR *src, z_size_t n) { + uchf *p = dst; + const uchf *q = src; + while (n) { + *p++ = *q++; + n--; + } } -int ZLIB_INTERNAL zmemcmp(const Bytef* s1, const Bytef* s2, uInt len) { - uInt j; - - for (j = 0; j < len; j++) { - if (s1[j] != s2[j]) return 2*(s1[j] > s2[j])-1; +int ZLIB_INTERNAL zmemcmp(const void FAR *s1, const void FAR *s2, z_size_t n) { + const uchf *p = s1, *q = s2; + while (n) { + if (*p++ != *q++) + return (int)p[-1] - (int)q[-1]; + n--; } return 0; } -void ZLIB_INTERNAL zmemzero(Bytef* dest, uInt len) { - if (len == 0) return; - do { - *dest++ = 0; /* ??? to be unrolled */ - } while (--len != 0); +void ZLIB_INTERNAL zmemzero(void FAR *b, z_size_t len) { + uchf *p = b; + while (len) { + *p++ = 0; + len--; + } } + #endif #ifndef Z_SOLO diff --git a/packages/runtime/vendor/zlib/zutil.h b/packages/runtime/vendor/zlib/zutil.h index 48dd7feba..64cd7a41e 100644 --- a/packages/runtime/vendor/zlib/zutil.h +++ b/packages/runtime/vendor/zlib/zutil.h @@ -1,5 +1,5 @@ /* zutil.h -- internal interface and configuration of the compression library - * Copyright (C) 1995-2024 Jean-loup Gailly, Mark Adler + * Copyright (C) 1995-2026 Jean-loup Gailly, Mark Adler * For conditions of distribution and use, see copyright notice in zlib.h */ @@ -28,6 +28,21 @@ # include # include #endif +#ifdef NO_ERRNO_H +# ifdef _WIN32_WCE + /* The Microsoft C Run-Time Library for Windows CE doesn't have + * errno. We define it as a global variable to simplify porting. + * Its value is always 0 and should not be used. We rename it to + * avoid conflict with other libraries that use the same workaround. + */ +# define errno z_errno +# endif + extern int errno; +#else +# ifndef _WIN32_WCE +# include +# endif +#endif #ifndef local # define local static @@ -36,6 +51,10 @@ define "local" for the non-static meaning of "static", for readability (compile with -Dlocal if your debugger can't find static symbols) */ +extern const char deflate_copyright[]; +extern const char inflate_copyright[]; +extern const char inflate9_copyright[]; + typedef unsigned char uch; typedef uch FAR uchf; typedef unsigned short ush; @@ -48,6 +67,8 @@ typedef unsigned long ulg; # define Z_U8 unsigned long # elif (ULLONG_MAX == 0xffffffffffffffff) # define Z_U8 unsigned long long +# elif (ULONG_LONG_MAX == 0xffffffffffffffff) +# define Z_U8 unsigned long long # elif (UINT_MAX == 0xffffffffffffffff) # define Z_U8 unsigned # endif @@ -63,7 +84,9 @@ extern z_const char * const z_errmsg[10]; /* indexed by 2-zlib_error */ /* To be used only when the state is known to be valid */ /* common constants */ - +#if MAX_WBITS < 9 || MAX_WBITS > 15 +# error MAX_WBITS must be in 9..15 +#endif #ifndef DEF_WBITS # define DEF_WBITS MAX_WBITS #endif @@ -141,7 +164,7 @@ extern z_const char * const z_errmsg[10]; /* indexed by 2-zlib_error */ # define OS_CODE 7 #endif -#ifdef __acorn +#if defined(__acorn) || defined(__riscos) # define OS_CODE 13 #endif @@ -168,11 +191,10 @@ extern z_const char * const z_errmsg[10]; /* indexed by 2-zlib_error */ #endif /* provide prototypes for these when building zlib without LFS */ -#if !defined(_WIN32) && \ - (!defined(_LARGEFILE64_SOURCE) || _LFS64_LARGEFILE-0 == 0) - ZEXTERN uLong ZEXPORT adler32_combine64(uLong, uLong, z_off_t); - ZEXTERN uLong ZEXPORT crc32_combine64(uLong, uLong, z_off_t); - ZEXTERN uLong ZEXPORT crc32_combine_gen64(z_off_t); +#ifndef Z_LARGE64 + ZEXTERN uLong ZEXPORT adler32_combine64(uLong, uLong, z_off64_t); + ZEXTERN uLong ZEXPORT crc32_combine64(uLong, uLong, z_off64_t); + ZEXTERN uLong ZEXPORT crc32_combine_gen64(z_off64_t); #endif /* common defaults */ @@ -211,9 +233,9 @@ extern z_const char * const z_errmsg[10]; /* indexed by 2-zlib_error */ # define zmemzero(dest, len) memset(dest, 0, len) # endif #else - void ZLIB_INTERNAL zmemcpy(Bytef* dest, const Bytef* source, uInt len); - int ZLIB_INTERNAL zmemcmp(const Bytef* s1, const Bytef* s2, uInt len); - void ZLIB_INTERNAL zmemzero(Bytef* dest, uInt len); + void ZLIB_INTERNAL zmemcpy(void FAR *, const void FAR *, z_size_t); + int ZLIB_INTERNAL zmemcmp(const void FAR *, const void FAR *, z_size_t); + void ZLIB_INTERNAL zmemzero(void FAR *, z_size_t); #endif /* Diagnostic functions */ @@ -251,4 +273,80 @@ extern z_const char * const z_errmsg[10]; /* indexed by 2-zlib_error */ #define ZSWAP32(q) ((((q) >> 24) & 0xff) + (((q) >> 8) & 0xff00) + \ (((q) & 0xff00) << 8) + (((q) & 0xff) << 24)) +#ifdef _MSC_VER +#define zalign(x) __declspec(align(x)) +#else +#define zalign(x) __attribute__((aligned((x)))) +#endif + +#ifdef Z_ONCE +/* + Create a local z_once() function depending on the availability of atomics. + */ + +/* Check for the availability of atomics. */ +#if defined(__STDC__) && __STDC_VERSION__ >= 201112L && \ + !defined(__STDC_NO_ATOMICS__) + +#include +typedef struct { + atomic_flag begun; + atomic_int done; +} z_once_t; +#define Z_ONCE_INIT {ATOMIC_FLAG_INIT, 0} + +/* + Run the provided init() function exactly once, even if multiple threads + invoke once() at the same time. The state must be a once_t initialized with + Z_ONCE_INIT. + */ +local void z_once(z_once_t *state, void (*init)(void)) { + if (!atomic_load(&state->done)) { + if (atomic_flag_test_and_set(&state->begun)) + while (!atomic_load(&state->done)) + ; + else { + init(); + atomic_store(&state->done, 1); + } + } +} + +#else /* no atomics */ + +#warning zlib not thread-safe + +typedef struct z_once_s { + volatile int begun; + volatile int done; +} z_once_t; +#define Z_ONCE_INIT {0, 0} + +/* Test and set. Alas, not atomic, but tries to limit the period of + vulnerability. */ +local int test_and_set(int volatile *flag) { + int was; + + was = *flag; + *flag = 1; + return was; +} + +/* Run the provided init() function once. This is not thread-safe. */ +local void z_once(z_once_t *state, void (*init)(void)) { + if (!state->done) { + if (test_and_set(&state->begun)) + while (!state->done) + ; + else { + init(); + state->done = 1; + } + } +} + +#endif /* ?atomics */ + +#endif /* Z_ONCE */ + #endif /* ZUTIL_H */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fc32620a..fe90bc447 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,19 +90,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: @@ -110,14 +159,18 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.9 - version: 0.0.9 + specifier: 0.0.10 + version: 0.0.10 typescript: specifier: 7.0.2 version: 7.0.2 typescript5: specifier: npm:typescript@5.9.3 version: typescript@5.9.3 + devDependencies: + typescript-7.1: + specifier: npm:typescript@7.1.0-beta + version: typescript@7.1.0-beta optionalDependencies: '@scriptc/llvm-darwin-arm64': specifier: workspace:* @@ -1360,8 +1413,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': @@ -1377,42 +1430,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.9': - resolution: {integrity: sha512-aIjSulef3DIEX7F3RRWLXyHWFgjXiVffBkrfE83ay/tpbsQaniu16cFxYGYNaqqVGft/oivecpdTDBbpPlsb8Q==} + '@tsc-ts/darwin-arm64@0.0.10': + resolution: {integrity: sha512-mRctKijCzWRxJgxiVAdsk+x9WB7kZz9GBbJ4eVP95bJFkIz9D11uw3xC0m6jWYNoaShF6zn48eLMTYOyJNxBdA==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.9': - resolution: {integrity: sha512-LHE74tzIEDtrEN8ENvCH/ejnnZjT0D2Vaz2HmggcvJudgr0W300CcwGf2Kdfw8MVBBk6JN0HPF9+a5AUUtFZGg==} + '@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.9': - resolution: {integrity: sha512-dNiQIKApLcKIJPzb3pk2gfGrrgmOWa+q3/qjrx5ia/SzIWz0+lENlVvHiIn9ukqDacDE30FW8xHzby8Kyd+10A==} + '@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.9': - resolution: {integrity: sha512-DolAqHtr1GUyG03Ch8+sJzpX1G+xzazAmj18kABzuOPre/6njrSWnmqJmtXQWvhwLBBEjajIkUJ4V+t5qigsrg==} + '@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.9': - resolution: {integrity: sha512-leANVZ4tKz3myBSVogb173ttbAurPOOGjy1WeBlRhkVJ7zOPMiyzJi3cPOAK/4O8RcxXOsCiC2AfKPKDYM8bEw==} + '@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.9': - resolution: {integrity: sha512-a4FW16QXkrwNHAr4Qo/qDA5w9LxCJNrYbpqiz3TJ1sePTqvW/JTJc14tjx6avhW4mIaQp8AB52m7TqNAuDMm4Q==} + '@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.9': - resolution: {integrity: sha512-phThYnmOinlvY4dS4DEl1L9t/mFLiHR1zL5plSUfMB5NizA/jESfgMGRu+HW3uwOpCETSxgsuTNAXO7UuEIHIQ==} + '@tsc-ts/win32-x64-msvc@0.0.10': + resolution: {integrity: sha512-TsiK6k0ljFW3pNR5lvF7SL0nv3aGxr31ZbBCfIXcBrqAvpumINOy/G0ESfTvZWx8llwOQt3PxBS414gF/J0tFA==} cpu: [x64] os: [win32] @@ -1446,120 +1499,246 @@ packages: cpu: [ppc64] os: [aix] + '@typescript/typescript-aix-ppc64@7.1.0-beta': + resolution: {integrity: sha512-DXDG+jIWxBHGdoO70tcHON33rCnSzsEZj9P13oyf/r/KEGIo6awsdSNVNjAw0uMwEpqRu75IrgCcW58YcVkZHg==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-android-arm64@7.1.0-beta': + resolution: {integrity: sha512-UZx6PxoQavuvaVTrSf3aw0Oy/J/uJpRylipA0kzc038jMvZXh0UtE8vnQp20FqtdgD5bhXZ0aJrRJyoTSUO58Q==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [android] + '@typescript/typescript-darwin-arm64@7.0.2': resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] + '@typescript/typescript-darwin-arm64@7.1.0-beta': + resolution: {integrity: sha512-q7mxblcyX0E7hXsY4k+Q6KWKK5mGmfVq+BDesdTgRVgjodk27aIxER77Zyj9+TgSTlCje8F7+JM6f6hl2BuNdw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + '@typescript/typescript-darwin-x64@7.0.2': resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] + '@typescript/typescript-darwin-x64@7.1.0-beta': + resolution: {integrity: sha512-D5EYqG5Wm/guw9+0j8+Mn1Jbr/gfS/VtFTuoH8PJtFbcqeDaQGMulc7SW8q51QG6Y3wGOGX3nbsTOw6fIKWWAA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + '@typescript/typescript-freebsd-arm64@7.0.2': resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [freebsd] + '@typescript/typescript-freebsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-9A0XX0tcnH9qPdolSKKzSjTb93dlB0UEcAJbHYEsrFX2fkFNSum9+dvkQQJ9vLc+7vxHetDPujq9Qe6wx+Xhow==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + '@typescript/typescript-freebsd-x64@7.0.2': resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [freebsd] + '@typescript/typescript-freebsd-x64@7.1.0-beta': + resolution: {integrity: sha512-EDf/dBHfY0TxX3NmUTek0hbffeyZlh3+ls6WQW3ICB8vYxwYLFnzg7wkgEUvl9eRlpiJ/nmc2S8arPMzINJ7rg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + '@typescript/typescript-linux-arm64@7.0.2': resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] + '@typescript/typescript-linux-arm64@7.1.0-beta': + resolution: {integrity: sha512-hNqQBgQ5b5oGqN76QqbAU6CwYhJZ6iOcWc2kPTUs2OentgpxZktETziH6KIyRFJPaiBncognwn2ZLu3DNhHiWw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + '@typescript/typescript-linux-arm@7.0.2': resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] + '@typescript/typescript-linux-arm@7.1.0-beta': + resolution: {integrity: sha512-AvN9HmzOQh46G4312ooSgMHhbB7DtnLwg8D9mo324X49B7weMwtJrBG1MxT9IyI8kzDd7YRtW+1hWGun7WG7Qw==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + '@typescript/typescript-linux-loong64@7.0.2': resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} engines: {node: '>=16.20.0'} cpu: [loong64] os: [linux] + '@typescript/typescript-linux-loong64@7.1.0-beta': + resolution: {integrity: sha512-xbUustlBrFflt2pNEyG5ye+d4WGPc/rfr2xK29MXeobPSw297JqwuKRrvbKM5o3fE/eUM/c+urVm6jQ3IaMVTw==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + '@typescript/typescript-linux-mips64el@7.0.2': resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} engines: {node: '>=16.20.0'} cpu: [mips64el] os: [linux] + '@typescript/typescript-linux-mips64el@7.1.0-beta': + resolution: {integrity: sha512-fS4/G7/0bXZ3NSq3nBMAXyg65I3tO4S7j08VNQ2g1aNC/qq79gE7Qne1BNjVsjMFfHBVBFjQYoTQCjmVU2iFlg==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + '@typescript/typescript-linux-ppc64@7.0.2': resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} engines: {node: '>=16.20.0'} cpu: [ppc64] os: [linux] + '@typescript/typescript-linux-ppc64@7.1.0-beta': + resolution: {integrity: sha512-0upCCxYPX8X6gOwkvfv+Ee7/ufyy7k97mTrGE0e5BxLlnUEa+w+rtgnv2dsKcxFdwdeZYVlZKCqgN/xCBV08+A==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + '@typescript/typescript-linux-riscv64@7.0.2': resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} engines: {node: '>=16.20.0'} cpu: [riscv64] os: [linux] + '@typescript/typescript-linux-riscv64@7.1.0-beta': + resolution: {integrity: sha512-0jKU9YRgOPRgiy5V/UWv37kB53PaTpIbwGCbbOMUPPAkrjVRNOIAJaz7Wa7s3H7mdDebX1LopUEPEJR+7Xa7zg==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + '@typescript/typescript-linux-s390x@7.0.2': resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} engines: {node: '>=16.20.0'} cpu: [s390x] os: [linux] + '@typescript/typescript-linux-s390x@7.1.0-beta': + resolution: {integrity: sha512-j+d04s2kxaPQh6DLPb01sknAQfzsrdSZy15jBRHyLEZoaaQ19dVjbFRlkNVvYwUG7ZdP8+y5b/7RmPxdtHUKdQ==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + '@typescript/typescript-linux-x64@7.0.2': resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] + '@typescript/typescript-linux-x64@7.1.0-beta': + resolution: {integrity: sha512-kcD51ihf+pnI5KT4uQQYG04OpwznaZYBlIQhDhl8fflFd27NktxXsUeguxZyvUMyWfum7IF2bBS6Jv7ox0AgFQ==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + '@typescript/typescript-netbsd-arm64@7.0.2': resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [netbsd] + '@typescript/typescript-netbsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-8W85YyoGK+rgGoSU9jJQUpELbrKcZcbdJk2jPNQ6UKDKI7ryPlPFTrV2jCE5RQuv4Tt96hMDTc33s4YfRrLFBA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + '@typescript/typescript-netbsd-x64@7.0.2': resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [netbsd] + '@typescript/typescript-netbsd-x64@7.1.0-beta': + resolution: {integrity: sha512-iGsTE0ve2PG0AcyZinwAR0LnGEZhu/y4dceS0hZsYCaKdYETvQtvtlNWlmYKempsBr6C7Il7dMXHVk+VFm9u0g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + '@typescript/typescript-openbsd-arm64@7.0.2': resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [openbsd] + '@typescript/typescript-openbsd-arm64@7.1.0-beta': + resolution: {integrity: sha512-Z2FERdHB9fAKI+KGqdWn04O7uNlnB+VYiEm6INE1aZuzzLNd3KBqQ6IpKUJF2ny4GigGU6nVu75XD9tAUODPng==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + '@typescript/typescript-openbsd-x64@7.0.2': resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [openbsd] + '@typescript/typescript-openbsd-x64@7.1.0-beta': + resolution: {integrity: sha512-NVG3qeyCq/rJEOhaRjR3LsWgjrjo5vJW25omVLeHAVnpKIG4pE4cenwwKGR06DiJ1s0QTOymcJeKXMEpsiNBVg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + '@typescript/typescript-sunos-x64@7.0.2': resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [sunos] + '@typescript/typescript-sunos-x64@7.1.0-beta': + resolution: {integrity: sha512-ovAdoWXEn2B5/fyWG69pLU3FWmQwPWOjQV5TFbcxtpnOv+R3g8ySGHl1NYRq7Lb/GYdFDvaGcZ+SiU7iKuiMsA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + '@typescript/typescript-win32-arm64@7.0.2': resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] + '@typescript/typescript-win32-arm64@7.1.0-beta': + resolution: {integrity: sha512-lJbo71IEOfL7ZZIZnArRaOLqjn9BAfoSzyUQn0aSjfSpDZL4ru0afUsdBfo6PQEjD6ssdmi0n/RwaPADX6bsvQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + '@typescript/typescript-win32-x64@7.0.2': resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] + '@typescript/typescript-win32-x64@7.1.0-beta': + resolution: {integrity: sha512-UKcHvbybY2tDTvFW2b1vwiAvDlIRooo69lh8CXWVhOQejcvpM5+IgbiFW9EIwR5Wi5X/v+BdN7gdWbigBuJ3rw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@vercel/backends@0.8.27': resolution: {integrity: sha512-WW9hfF0mWZDQr23RcjLgVUIoo3V07aq25t6f3RSbq0yIQ2zKZ41x8+0nhNKjzAP7GB7yFmwX4UOv4wV2fWqDDQ==} @@ -2577,8 +2756,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.9: - resolution: {integrity: sha512-7fDfC/o1YoITG6sSXj4ZznijQYg8AfoCagmYvwG1BFLR/hz4Lwnq7jqDwcZfoDuPYTkrs8jHUTAVG1turniJHA==} + tsc-ts@0.0.10: + resolution: {integrity: sha512-oF5hZbeNgJXwJ868adnvBAihxYUwra1rj6Sa2+UhuhW477t5MmGnOsZgGTSGz511uTfuUxDz6e/gLIzvvScbGw==} engines: {node: '>=20'} hasBin: true @@ -2605,6 +2784,11 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + typescript@7.1.0-beta: + resolution: {integrity: sha512-cjWjZAJDsmZzsNC5GwHNlW0YPM65jZa5oR0KyQcYX3KSVIfTvtBq//vewISyatKk4OLi5grZjHhIVE9fdrw1UQ==} + engines: {node: '>=16.20.0'} + hasBin: true + uid-promise@1.0.0: resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==} @@ -3439,7 +3623,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': {} @@ -3454,25 +3638,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.9': + '@tsc-ts/darwin-arm64@0.0.10': optional: true - '@tsc-ts/darwin-x64@0.0.9': + '@tsc-ts/darwin-x64@0.0.10': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.9': + '@tsc-ts/linux-arm64-gnu@0.0.10': optional: true - '@tsc-ts/linux-arm64-musl@0.0.9': + '@tsc-ts/linux-arm64-musl@0.0.10': optional: true - '@tsc-ts/linux-x64-gnu@0.0.9': + '@tsc-ts/linux-x64-gnu@0.0.10': optional: true - '@tsc-ts/linux-x64-musl@0.0.9': + '@tsc-ts/linux-x64-musl@0.0.10': optional: true - '@tsc-ts/win32-x64-msvc@0.0.9': + '@tsc-ts/win32-x64-msvc@0.0.10': optional: true '@tybys/wasm-util@0.10.3': @@ -3506,63 +3690,126 @@ snapshots: '@typescript/typescript-aix-ppc64@7.0.2': optional: true + '@typescript/typescript-aix-ppc64@7.1.0-beta': + optional: true + + '@typescript/typescript-android-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-darwin-arm64@7.0.2': optional: true + '@typescript/typescript-darwin-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-darwin-x64@7.0.2': optional: true + '@typescript/typescript-darwin-x64@7.1.0-beta': + optional: true + '@typescript/typescript-freebsd-arm64@7.0.2': optional: true + '@typescript/typescript-freebsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-freebsd-x64@7.0.2': optional: true + '@typescript/typescript-freebsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-arm64@7.0.2': optional: true + '@typescript/typescript-linux-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-arm@7.0.2': optional: true + '@typescript/typescript-linux-arm@7.1.0-beta': + optional: true + '@typescript/typescript-linux-loong64@7.0.2': optional: true + '@typescript/typescript-linux-loong64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-mips64el@7.0.2': optional: true + '@typescript/typescript-linux-mips64el@7.1.0-beta': + optional: true + '@typescript/typescript-linux-ppc64@7.0.2': optional: true + '@typescript/typescript-linux-ppc64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-riscv64@7.0.2': optional: true + '@typescript/typescript-linux-riscv64@7.1.0-beta': + optional: true + '@typescript/typescript-linux-s390x@7.0.2': optional: true + '@typescript/typescript-linux-s390x@7.1.0-beta': + optional: true + '@typescript/typescript-linux-x64@7.0.2': optional: true + '@typescript/typescript-linux-x64@7.1.0-beta': + optional: true + '@typescript/typescript-netbsd-arm64@7.0.2': optional: true + '@typescript/typescript-netbsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-netbsd-x64@7.0.2': optional: true + '@typescript/typescript-netbsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-openbsd-arm64@7.0.2': optional: true + '@typescript/typescript-openbsd-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-openbsd-x64@7.0.2': optional: true + '@typescript/typescript-openbsd-x64@7.1.0-beta': + optional: true + '@typescript/typescript-sunos-x64@7.0.2': optional: true + '@typescript/typescript-sunos-x64@7.1.0-beta': + optional: true + '@typescript/typescript-win32-arm64@7.0.2': optional: true + '@typescript/typescript-win32-arm64@7.1.0-beta': + optional: true + '@typescript/typescript-win32-x64@7.0.2': optional: true + '@typescript/typescript-win32-x64@7.1.0-beta': + optional: true + '@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: '@vercel/build-utils': 13.36.0 @@ -4813,17 +5060,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.9: + tsc-ts@0.0.10: dependencies: - '@scriptc/threads': 0.4.2 + '@scriptc/threads': 0.4.5 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.9 - '@tsc-ts/darwin-x64': 0.0.9 - '@tsc-ts/linux-arm64-gnu': 0.0.9 - '@tsc-ts/linux-arm64-musl': 0.0.9 - '@tsc-ts/linux-x64-gnu': 0.0.9 - '@tsc-ts/linux-x64-musl': 0.0.9 - '@tsc-ts/win32-x64-msvc': 0.0.9 + '@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 @@ -4866,6 +5113,30 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 + typescript@7.1.0-beta: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.1.0-beta + '@typescript/typescript-android-arm64': 7.1.0-beta + '@typescript/typescript-darwin-arm64': 7.1.0-beta + '@typescript/typescript-darwin-x64': 7.1.0-beta + '@typescript/typescript-freebsd-arm64': 7.1.0-beta + '@typescript/typescript-freebsd-x64': 7.1.0-beta + '@typescript/typescript-linux-arm': 7.1.0-beta + '@typescript/typescript-linux-arm64': 7.1.0-beta + '@typescript/typescript-linux-loong64': 7.1.0-beta + '@typescript/typescript-linux-mips64el': 7.1.0-beta + '@typescript/typescript-linux-ppc64': 7.1.0-beta + '@typescript/typescript-linux-riscv64': 7.1.0-beta + '@typescript/typescript-linux-s390x': 7.1.0-beta + '@typescript/typescript-linux-x64': 7.1.0-beta + '@typescript/typescript-netbsd-arm64': 7.1.0-beta + '@typescript/typescript-netbsd-x64': 7.1.0-beta + '@typescript/typescript-openbsd-arm64': 7.1.0-beta + '@typescript/typescript-openbsd-x64': 7.1.0-beta + '@typescript/typescript-sunos-x64': 7.1.0-beta + '@typescript/typescript-win32-arm64': 7.1.0-beta + '@typescript/typescript-win32-x64': 7.1.0-beta + uid-promise@1.0.0: {} undici-types@5.26.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c9f873c4e..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.9" - - "@tsc-ts/darwin-x64@0.0.9" - - "@tsc-ts/linux-arm64-gnu@0.0.9" - - "@tsc-ts/linux-arm64-musl@0.0.9" - - "@tsc-ts/linux-x64-gnu@0.0.9" - - "@tsc-ts/linux-x64-musl@0.0.9" - - "@tsc-ts/win32-x64-msvc@0.0.9" - - "tsc-ts@0.0.9" - - "@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/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-native-cli.mts b/scripts/build-native-cli.mts index 05f36cf68..22b9967ba 100644 --- a/scripts/build-native-cli.mts +++ b/scripts/build-native-cli.mts @@ -6,7 +6,11 @@ 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"; @@ -14,6 +18,9 @@ import type { NativeToolchainManifest } from "../packages/compiler/src/native/to 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 @@ -24,10 +31,10 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1"; // 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. +// has lost most of its gain. A profile refreshed on the same commit, with +// the same layout, matches all of the native compiler's profiled functions. +// One refreshed 1.5 days and about 200 commits earlier matched 87.2%, and +// 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"]; @@ -37,6 +44,19 @@ 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 uses none. Instrumented builds use the same +// layout as the builds their profile is for. The layout decides which +// functions share a partition, and a function whose partition differs from +// the instrumented build's is optimized differently before its control flow +// is hashed, so the profile's record for it no longer matches. +const committedLayout = join(root, "packages/compiler/native/cli.layout.gz"); +const requestedLayout = process.env["SCRIPTC_NATIVE_LAYOUT"]; +const functionLayout = + 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"); @@ -94,12 +114,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"), @@ -116,16 +138,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) @@ -167,6 +193,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. diff --git a/scripts/generate-ir-fields.mjs b/scripts/generate-ir-fields.mjs new file mode 100644 index 000000000..95ce5d217 --- /dev/null +++ b/scripts/generate-ir-fields.mjs @@ -0,0 +1,296 @@ +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 samePath = (a, b) => a.replace(/\\/g, "/") === b.replace(/\\/g, "/"); + +/** The text of fields.generated.ts for `irText`, the source of ir.ts. */ +export function generateIrFields(irText = readFileSync(irPath, "utf8")) { + const options = { strict: true, target: ts.ScriptTarget.ES2022 }; + const host = ts.createCompilerHost(options); + const readSourceFile = host.getSourceFile; + // ir.ts is read from `irText`; the files it imports from disk. + host.getSourceFile = (file, languageVersion, ...rest) => + samePath(file, irPath) + ? ts.createSourceFile(file, irText, languageVersion) + : readSourceFile.call(host, file, languageVersion, ...rest); + const program = ts.createProgram([irPath], options, host); + 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; + + /** Whether every value of `type` is a number, boolean, bigint, symbol, null + * or undefined. A record of such fields walks to nothing too, but it is an + * object: its own fields are checked instead. */ + function onlyScalar(type) { + const parts = type.isUnion() ? type.types : [type]; + return parts.every((part) => (part.flags & (scalar | nullish)) !== 0); + } + + /** 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; + /** The fields each walked object type declares, for the type-level check at + * the end of the file: `typeExpr` names the object type, `fields` every + * field it had when the file was generated and `scalars` the fields that held + * no string and no object then. */ + const checks = []; + /** Records `property list` of `object` (named `typeExpr`) for the check. */ + function noteFields(typeExpr, properties, scalarNames) { + checks.push({ + typeExpr, + fields: properties.map((property) => property.name), + scalars: scalarNames, + }); + } + /** Lines that walk `expr` of `type` (named `typeExpr` in the generated file): + * strings to `walker.string`, IR nodes to their walkers, arrays by element and + * other objects by field. */ + function walkLines(expr, type, typeExpr, 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}) {`); + const itemType = `Extract, readonly unknown[]>[number]`; + body.push( + ...walkLines(item, checker.getTypeArguments(objects[0])[0], itemType, " ", depth + 1), + ); + body.push("}"); + } else if (kinds.has(null) && objects.length === 1) { + const recordType = `Extract, object>`; + const properties = checker.getPropertiesOfType(objects[0]); + const scalarNames = []; + for (const property of properties) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `${expr}.${property.name}`; + const fieldTypeExpr = `${recordType}[${JSON.stringify(property.name)}]`; + const lines = walkLines(field, fieldType, fieldTypeExpr, "", depth + 1); + if (onlyScalar(fieldType)) scalarNames.push(property.name); + if (lines.length === 0) continue; + if (property.name === "type") + body.push("if (!skipType) {", ...lines.map((l) => ` ${l}`), "}"); + else body.push(...lines); + } + noteFields(recordType, properties, scalarNames); + } 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)}: {`); + const memberType = `Extract<${name}, { kind: ${JSON.stringify(kind)} }>`; + const scalarNames = []; + for (const property of member.getProperties()) { + if (property.name === "loc") continue; + const fieldType = checker.getTypeOfSymbol(property); + const field = `node.${property.name}`; + const fieldTypeExpr = `${memberType}[${JSON.stringify(property.name)}]`; + const walk = walkLines(field, fieldType, fieldTypeExpr, "", 0); + if (onlyScalar(fieldType)) scalarNames.push(property.name); + 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;", " }"); + noteFields(memberType, member.getProperties(), scalarNames); + } + lines.push(" }", "}", ""); + } + + // Type checking fails below when ir.ts gives a walked object a field this + // file does not know, or lets a field that held no string and no object hold + // one before this file is regenerated. + const quoted = (names) => names.map((name) => JSON.stringify(name)).join(" | "); + lines.push( + "/** A field ir.ts declares that this file does not read. */", + "type FieldsNotRead = Exclude;", + "/** The string or object values a field read as a scalar can hold. */", + "type ValuesNotRead = Extract, string | object>;", + "/** Fails type checking unless `T` is never: run `pnpm generate:ir-fields`. */", + "type Unread = T;", + "", + "/** Every walked object type's fields as ir.ts declared them when this file", + " * was generated. */", + "export type IrFieldsCurrent = [", + ); + for (const { typeExpr, fields, scalars } of checks) { + lines.push( + ` Unread 0 ? quoted(fields) : "never"}>>,`, + ); + for (const scalarName of scalars) + lines.push(` Unread>,`); + } + lines.push("];", ""); + return lines.join("\n"); +} + +if (process.argv[1] !== undefined && samePath(process.argv[1], fileURLToPath(import.meta.url))) { + const text = generateIrFields(); + + 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/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 dde4e21b9..a9436b505 100644 --- a/scripts/refresh-cli-profile.mjs +++ b/scripts/refresh-cli-profile.mjs @@ -6,7 +6,7 @@ * * 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` + * indexed profile. The training builds are tsc-ts (a cold `speed` * 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 @@ -14,6 +14,11 @@ * move much compiler code; a stale profile only loses part of its gain. A * refresh takes about 11 minutes on 16 cores. * + * The instrumented compiler is built with the committed function layout, or + * the one SCRIPTC_NATIVE_LAYOUT names, and the profile matches builds that + * use the same layout. When refreshing both, refresh the layout first + * (scripts/refresh-cli-layout.mjs). + * * node scripts/refresh-cli-profile.mjs --tsc-ts [--jobs 8] */ import { spawn, spawnSync } from "node:child_process"; @@ -210,12 +215,15 @@ const profiles = readdirSync(raw) .map((name) => join(raw, name)); rmSync(builds, { recursive: true, force: true }); const indexed = join(work, "cli.profdata"); -const merged = spawnSync(scriptc, ["merge-profiles", "--sparse", "-o", indexed, ...profiles], { +// The profile keeps the functions that never ran in training, with zero +// counts. Builds optimize those as cold and place them after the trained code, +// which keeps the compiler's .text smaller than a sparse profile does. +const merged = spawnSync(scriptc, ["merge-profiles", "-o", indexed, ...profiles], { stdio: "inherit", env: { ...trainingEnv, LLVM_PROFILE_FILE: join(work, "merge.profraw") }, }); if (merged.status !== 0) throw new Error("merge-profiles failed"); -// The indexed profile compresses to about an eighth of its size. +// The indexed profile compresses to about a sixth of its size. writeFileSync( profileOutput, gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }), diff --git a/scripts/registry-claims.mjs b/scripts/registry-claims.mjs new file mode 100644 index 000000000..ac561a2b8 --- /dev/null +++ b/scripts/registry-claims.mjs @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/* Lists the numbers that open pull requests claim in the repository's + * numbered registries, and reports the ones two changes claim differently: + * + * diagnostic codes a code an added line names that origin/main has no + * row for in packages/compiler/src/diagnostics/codes.ts + * dyn handle tags an added pinned row X(NAME, value) in + * packages/runtime/src/scr_dyn.h + * + * The current branch (its changes since it left origin/main) counts as one + * more claimant. Run it before picking a number, or before pushing a change + * that takes one; it needs `gh` access to the repository. + * + * node scripts/registry-claims.mjs every claim and every collision + * node scripts/registry-claims.mjs --check only collisions with this branch; + * exits 1 when there is one + * + * A pull request whose diff cannot be read is named in the output, and the + * run exits 2 (unless it found a collision), since its claims are missing. */ +import { execFile, execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { promisify } from "node:util"; + +const run = promisify(execFile); +const check = process.argv.includes("--check"); +const git = (...args) => + execFileSync("git", args, { + encoding: "utf8", + maxBuffer: 1 << 30, + stdio: ["ignore", "pipe", "pipe"], + }); + +const CODES = "packages/compiler/src/diagnostics/codes.ts"; +const DYN = "packages/runtime/src/scr_dyn.h"; + +git("fetch", "-q", "origin", "main"); +// The codes origin/main has: its active and retired rows (a reserved code is +// still a claim), or, before codes.ts existed there, every code its sources name. +const baseCodes = new Map(); +const codesSource = (() => { + try { + return git("show", `origin/main:${CODES}`); + } catch { + return null; + } +})(); +if (codesSource !== null) { + const reserved = codesSource.indexOf("export const RESERVED_DIAGNOSTIC_CODES"); + for (const m of codesSource.matchAll(/^ {2}(SC\d{4}): "([^"]*)"/gm)) { + if (reserved < 0 || m.index < reserved) baseCodes.set(m[1], m[2]); + } +} else { + const named = git("grep", "-ohE", "SC[0-9]{4}", "origin/main", "--", "packages/*/src/**"); + for (const code of named.split("\n").filter(Boolean)) baseCodes.set(code, ""); +} +const baseTags = new Map( + [...git("show", `origin/main:${DYN}`).matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map( + ([, name, value]) => [Number(value), name], + ), +); + +/** The claims of one unified diff: [registry, number, label] triples. */ +function claimsOf(diff) { + const claims = []; + let file = ""; + let table = ""; // the codes.ts table the current line is in + for (const line of diff.split("\n")) { + if (line.startsWith("+++ ")) { + file = line.slice(4).replace(/^b\//, ""); + continue; + } + const section = /^(?:@@.*@@ |[ +])export const (\w+)/.exec(line); + if (section) table = section[1]; + if (!line.startsWith("+")) continue; + // A reserved row holds a code for other work; it is not this change's claim. + if (file === CODES && table === "RESERVED_DIAGNOSTIC_CODES") continue; + if (file === DYN) { + const tag = /^\+\s*X\(([A-Z0-9_]+), (\d+)\)/.exec(line); + if (tag) claims.push(["dyn handle tag", Number(tag[2]), tag[1]]); + } + if (!file.startsWith("packages/")) continue; + const row = file === CODES ? /^\+ {2}(SC\d{4}): "([^"]*)"/.exec(line) : null; + if (row) { + if (baseCodes.get(row[1]) !== row[2]) claims.push(["diagnostic code", row[1], row[2]]); + continue; + } + for (const [code] of line.matchAll(/\bSC\d{4}\b/g)) { + if (!baseCodes.has(code)) + claims.push(["diagnostic code", code, `${file}: ${line.slice(1).trim()}`]); + } + } + return claims; +} + +const prs = JSON.parse( + ( + await run("gh", [ + "pr", + "list", + "--state", + "open", + "--limit", + "300", + "--json", + "number,title,headRefName,baseRefName", + ]) + ).stdout, +); +const head = git("rev-parse", "--abbrev-ref", "HEAD").trim(); +const here = `this branch (${head})`; + +// A stack of pull requests (each based on the previous one's branch) is one +// claimant: a later PR's diff only shows what it adds to its base. +const byHead = new Map(prs.map((pr) => [pr.headRefName, pr])); +const stackOf = (pr) => { + while (byHead.has(pr.baseRefName) && byHead.get(pr.baseRefName) !== pr) + pr = byHead.get(pr.baseRefName); + return pr.headRefName; +}; +const stacks = new Map(); +const claimant = (key) => stacks.get(key) ?? stacks.set(key, { names: [], claims: [] }).get(key); + +const queue = [...prs]; +// Pull requests whose diff could not be read: their claims are missing. +const unread = []; +await Promise.all( + Array.from({ length: 8 }, async () => { + for (let pr = queue.shift(); pr; pr = queue.shift()) { + const diff = await run("gh", ["pr", "diff", String(pr.number)], { maxBuffer: 1 << 30 }).then( + (r) => r.stdout, + (error) => { + const reason = String(error.stderr || error.message) + .trim() + .split("\n")[0]; + unread.push(`#${pr.number} (${reason})`); + return null; + }, + ); + const stack = claimant(stackOf(pr)); + stack.names.push({ number: pr.number, title: pr.title }); + if (diff !== null) stack.claims.push(...claimsOf(diff)); + } + }), +); +// This branch is its own PR's claimant when it has one open. +const mine = claimant(byHead.has(head) ? stackOf(byHead.get(head)) : head); +mine.names.push({ number: Infinity, title: here }); +// Committed and uncommitted changes since the branch left origin/main, and +// untracked files as wholly added. +const untracked = git("ls-files", "--others", "--exclude-standard", "-z") + .split("\0") + .filter(Boolean); +mine.claims.push( + ...claimsOf(git("diff", git("merge-base", "origin/main", "HEAD").trim())), + ...untracked + .map((file) => claimsOf(`+++ b/${file}\n` + readFileSync(file, "utf8").replace(/^/gm, "+"))) + .flat(), +); +// A stack reads "#1 #2 #3: ". +const sources = [...stacks.values()].map(({ names, claims }) => { + names.sort((a, b) => a.number - b.number); + const prs = names.filter((n) => n.number !== Infinity).map((n) => `#${n.number}`); + const title = names[0].title; + const name = prs.length === 0 ? title : `${prs.join(" ")}: ${title}`; + return { + name: names.some((n) => n.title === here) && prs.length > 0 ? `${name} (${here})` : name, + claims, + }; +}); + +// registry → number → label → claimants. Each claimant labels a number once: +// with the codes.ts row that claims it when it adds one, otherwise with the +// first line that names it. +const table = new Map(); +for (const { name, claims } of sources) { + const labelOf = new Map(); + for (const [registry, number, label] of claims) { + const key = `${registry}\0${number}`; + const row = registry === "dyn handle tag" || !label.includes(": "); + if (!labelOf.has(key) || (row && !labelOf.get(key).row)) + labelOf.set(key, { registry, number, label, row }); + } + for (const { registry, number, label } of labelOf.values()) { + const numbers = table.get(registry) ?? table.set(registry, new Map()).get(registry); + const labels = numbers.get(number) ?? numbers.set(number, new Map()).get(number); + (labels.get(label) ?? labels.set(label, []).get(label)).push(name); + } +} + +let collisions = 0; +for (const [registry, numbers] of [...table].sort()) { + for (const [number, labels] of [...numbers].sort(([a], [b]) => (a < b ? -1 : 1))) { + const onMain = registry === "dyn handle tag" ? baseTags.get(number) : undefined; + const clash = labels.size > 1 || (onMain !== undefined && !labels.has(onMain)); + const ours = [...labels.values()].some((who) => who.some((name) => name.includes(here))); + if (check && !(clash && ours)) continue; + if (clash) collisions++; + console.log(`${clash ? "COLLISION" : "claimed"} ${registry} ${number}`); + if (onMain !== undefined && !labels.has(onMain)) console.log(` origin/main: ${onMain}`); + for (const [label, who] of labels) console.log(` ${label}\n ${who.join("\n ")}`); + } +} +if (unread.length > 0) { + console.log( + `could not read the diff of ${unread.length} pull request${unread.length === 1 ? "" : "s"}; ${unread.length === 1 ? "its" : "their"} claims are missing:`, + ); + for (const pr of unread.sort()) console.log(` ${pr}`); +} +if (collisions === 0) + console.log( + `${check ? "no collisions with this branch" : "no collisions"}${unread.length > 0 ? " among the diffs read" : ""}`, + ); +process.exitCode = check && collisions > 0 ? 1 : unread.length > 0 ? 2 : 0; diff --git a/scripts/sandbox-bootstrap.sh b/scripts/sandbox-bootstrap.sh index ac0a4558a..ea50a7574 100644 --- a/scripts/sandbox-bootstrap.sh +++ b/scripts/sandbox-bootstrap.sh @@ -32,6 +32,7 @@ sudo env DEBIAN_FRONTEND=noninteractive apt-get install --quiet=2 --yes --no-ins libclang-rt-22-dev \ libpolly-22-dev \ libzstd-dev \ + lld-22 \ llvm-22-dev \ ninja-build \ xz-utils \ @@ -40,6 +41,9 @@ sudo rm -f /usr/local/bin/clang /usr/local/bin/clang++ sudo ln -sf clang-22 /usr/bin/clang sudo ln -sf clang++-22 /usr/bin/clang++ test "$(llvm-config-22 --version)" = 22.1.8 +# CI's pinned LLVM ships ld.lld and the compiler links with it when the +# driver finds it (beside clang's real executable): match that here. +test -x "$(dirname "$(readlink -f "$(command -v clang)")")/ld.lld" node_archive="node-v${node_version}-linux-x64.tar.xz" curl --fail --silent --show-error --location \ diff --git a/scripts/sandbox-config.mjs b/scripts/sandbox-config.mjs index 9ab1f6fa1..9b64eba57 100644 --- a/scripts/sandbox-config.mjs +++ b/scripts/sandbox-config.mjs @@ -291,3 +291,22 @@ export function sandboxTestSourceConfig(env) { createArgs: ["--image", image.sandboxImage], }; } + +/** A built checkout inside a prepared snapshot whose native artifacts a + * lane may reuse when their inputs match (SCRIPTC_SANDBOX_NATIVE_REFERENCE). + * It needs the snapshot's tools, so it is ignored for the managed image. */ +export function sandboxNativeReference(env) { + loadLocalEnv(); + const source = env ?? process.env; + const reference = source.SCRIPTC_SANDBOX_NATIVE_REFERENCE?.trim(); + if (!reference) return undefined; + if (!sandboxTestSourceConfig(source).prepared) { + throw new Error( + "SCRIPTC_SANDBOX_NATIVE_REFERENCE needs SCRIPTC_SANDBOX_SNAPSHOT or SCRIPTC_SANDBOX_IMAGE", + ); + } + if (!/^\/[\w./-]+$/.test(reference) || reference.split("/").includes("..")) { + throw new Error("SCRIPTC_SANDBOX_NATIVE_REFERENCE must be an absolute path without '..'"); + } + return reference.replace(/\/+$/, ""); +} diff --git a/scripts/sandbox-test.mjs b/scripts/sandbox-test.mjs index 602ef033e..3fe063075 100644 --- a/scripts/sandbox-test.mjs +++ b/scripts/sandbox-test.mjs @@ -9,6 +9,7 @@ import { parseArgs } from "node:util"; import { fileURLToPath } from "node:url"; import { sandboxBootstrapCommand, + sandboxNativeReference, sandboxTestSourceConfig, sandboxRunnerConfig, sandboxEffectWorkerAllocation, @@ -112,6 +113,38 @@ const hostInvariantFiles = [ "packages/runtime/test/tonumber.test.ts", "packages/runtime/test/url.test.ts", ]; +// Native artifacts a lane needs and every source path their builds read. +// Output paths are excluded from the input comparison by these names. +const NATIVE_OUTPUT_NAMES = [ + "bin", + "artifacts", + "runtime-pack.json", + ".runtime-pack-*", + "node_modules", + "dist", + "build", + ".cache", +]; +const LLVM_HELPER_INPUTS = ["native/llvm-codegen", "packages/llvm-linux-x64-gnu"]; +const NATIVE_ARTIFACTS = { + llvm: { + inputs: LLVM_HELPER_INPUTS, + outputs: ["packages/llvm-linux-x64-gnu/bin/scriptc-llvm-codegen"], + }, + runtime: { + inputs: [ + ...LLVM_HELPER_INPUTS, + "packages/runtime", + "packages/runtime-pack-common", + "packages/runtime-linux-x64-gnu", + "packages/runtime-darwin-arm64/scripts", + ], + outputs: [ + "packages/runtime-linux-x64-gnu/artifacts", + "packages/runtime-linux-x64-gnu/runtime-pack.json", + ], + }, +}; const hostSchedule = sandboxHostSchedule(process.platform, hostInvariantFiles); const nativeHostInvariantFiles = hostSchedule.localInvariantFiles; const remoteWorkspaceReset = workspaceResetCommand("/workspace"); @@ -188,6 +221,8 @@ Environment: VERCEL_TOKEN Access-token fallback; also set VERCEL_TEAM_ID + VERCEL_PROJECT_ID SCRIPTC_SANDBOX_IMAGE Optional fully qualified VCR image (default: vercel/sandbox/universal) SCRIPTC_SANDBOX_SNAPSHOT Optional prepared snapshot with the pinned toolchain (exclusive with IMAGE) + SCRIPTC_SANDBOX_NATIVE_REFERENCE Optional built checkout inside the snapshot; its LLVM helper + and runtime pack are reused when their inputs are identical SCRIPTC_SANDBOX_VCPUS vCPUs per sandbox (default: 8) SCRIPTC_SANDBOX_TIMEOUT sandbox and command timeout (default: 45m) SCRIPTC_TEST_WORKERS Vitest workers per sandbox (default: 4) @@ -197,6 +232,7 @@ Environment: } const sourceConfig = sandboxTestSourceConfig(); +const nativeReference = sandboxNativeReference(); const bootstrapCommand = sandboxBootstrapCommand(sourceConfig.prepared); const vercelConfig = sandboxVercelConfig(); const vercelProcessEnv = sandboxVercelEnvironment(vercelConfig); @@ -496,6 +532,42 @@ const execIn = async ( } }; +/** The command that provides one native artifact (`llvm`: the Linux LLVM + * helper, `runtime`: the Linux runtime pack). It builds the artifact unless + * SCRIPTC_SANDBOX_NATIVE_REFERENCE names a built checkout inside the Sandbox + * (a prepared snapshot's) whose inputs for that artifact are identical; then + * it copies the reference's artifact. The runtime build runs the helper, so + * its inputs include the helper's. */ +function nativeArtifactCommand(artifact) { + const pkg = + artifact === "llvm" ? "@scriptc/llvm-linux-x64-gnu" : "@scriptc/runtime-linux-x64-gnu"; + if (nativeReference === undefined) return ["pnpm", ["--filter", pkg, "build:native"]]; + const build = `pnpm --filter ${pkg} build:native`; + const { inputs, outputs } = NATIVE_ARTIFACTS[artifact]; + const excludes = NATIVE_OUTPUT_NAMES.map((name) => `-x '${name}'`).join(" "); + const ref = nativeReference; + const script = [ + "set -eu", + `ref='${ref}'`, + "same=1", + ...outputs.map((output) => `[ -e "$ref/${output}" ] || same=0`), + ...inputs.map( + (input) => + `[ "$same" = 0 ] || diff -rq ${excludes} "$ref/${input}" "${input}" >/dev/null 2>&1 || same=0`, + ), + 'if [ "$same" = 1 ]; then', + ...outputs.map( + (output) => + ` rm -rf "${output}" && mkdir -p "$(dirname "${output}")" && cp -R "$ref/${output}" "${output}"`, + ), + ` echo "reused the ${artifact} artifact from $ref: identical inputs"`, + "else", + ` ${build}`, + "fi", + ].join("\n"); + return ["sh", ["-c", script]]; +} + async function preflight() { console.log("Sandbox preflight:"); console.log(` auth: ${vercelConfig.authSource}`); @@ -582,7 +654,10 @@ async function createWorker(worker) { vcpus, "--non-persistent", ]; - for (let attempt = 1; attempt <= 2; attempt++) { + // Sandbox creation is rate limited per team in vCPUs per minute, so a + // large fan-out (many shards, large Sandboxes) retries with backoff. + const attempts = 6; + for (let attempt = 1; attempt <= attempts; attempt++) { try { await vercel(args, { idleTimeoutMs: 60_000, @@ -598,8 +673,12 @@ async function createWorker(worker) { await execIn(worker, "true", [], {}, "create status", 60_000, "/"); return; } catch (error) { - if (attempt === 2) throw error; - console.warn(`[${worker.label}] Sandbox is not reachable; retrying creation once...`); + if (attempt === attempts) throw error; + const delay = 15_000 * attempt; + console.warn( + `[${worker.label}] Sandbox is not reachable; retrying creation in ${delay / 1000}s...`, + ); + await new Promise((resolve) => setTimeout(resolve, delay)); } } } @@ -781,19 +860,13 @@ try { // including TypeScript's incremental metadata. Rebuild both together. await execIn(worker, "pnpm", ["build:fresh"], {}, "", 2 * 60_000); // Workspace builds deliberately do not rebuild packaged native artifacts. - // Every remote lane needs the Linux helper and runtime from this worktree. + // Every remote lane needs the Linux helper and runtime from this worktree, + // built from its sources or reused from a reference checkout whose + // native inputs are identical. + await execIn(worker, ...nativeArtifactCommand("llvm"), {}, "LLVM helper", 5 * 60_000); await execIn( worker, - "pnpm", - ["--filter", "@scriptc/llvm-linux-x64-gnu", "build:native"], - {}, - "LLVM helper", - 5 * 60_000, - ); - await execIn( - worker, - "pnpm", - ["--filter", "@scriptc/runtime-linux-x64-gnu", "build:native"], + ...nativeArtifactCommand("runtime"), { CC: "zig", AR: "zig" }, "runtime pack", 5 * 60_000, 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 310cc04a9..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", @@ -48,14 +68,11 @@ if (toolchain.lib_diagnostics !== undefined) { throw new Error("native toolchain needs a relative lib_diagnostics"); within(resolve(dirname(binary), hostPath(value))); } -const pack = JSON.parse( - readFileSync( - resolve(dirname(binary), hostPath(toolchain.runtime_pack), "runtime-pack.json"), - "utf8", - ), -); +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}`); @@ -91,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", @@ -111,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/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index df407bbb7..70d9fce6d 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -6,6 +6,7 @@ * the first unit that differs. The self-hosted compiler's IR and LLVM exceed * V8's maximum string length, so splitting works on Buffers and hashes each * unit instead of decoding the artifact. */ +import assert from "node:assert/strict"; import { createHash } from "node:crypto"; const hash = (bytes) => createHash("sha1").update(bytes).digest("hex"); @@ -401,7 +402,9 @@ export function dynamicTierSymbols(names) { * 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 + * and results the checker types `any`. bench/record-untyped-writes, also + * added later, writes typed records through `unknown` and `Object.assign` + * views. 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. */ @@ -425,6 +428,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/json-unions", "bench/throw-chains", "bench/regex-replacers", + "bench/record-untyped-writes", ]); /** Names of the functions (text and weak symbols) in `nm` output. Mach-O @@ -487,6 +491,57 @@ export function compareCounts(baseline, candidate) { }; } +/** The report lines the gate prints for one program. */ +export function programLines(entry) { + const lines = []; + const pct = (ratio) => `${ratio >= 1 ? "+" : ""}${((ratio - 1) * 100).toFixed(2)}%`; + const label = { + identical: "identical", + allowed: "allowed ", + differs: "DIFFERS ", + "build-error": "ERROR ", + "both-fail": "both-fail", + }[entry.verdict]; + const notes = []; + for (const [kind, result] of Object.entries(entry.artifacts)) + if (!result.identical) { + const counts = result.changed + ? ` (${result.changed.length} changed, ${result.added.length} added, ${result.removed.length} removed)` + : ""; + notes.push(`${kind}: ${result.first}${counts}`); + } + const size = entry.artifacts.exe?.bytes; + if (size && size.baseline !== size.candidate) + notes.push( + `size ${size.baseline} -> ${size.candidate} B (${pct(size.candidate / size.baseline)})`, + ); + if (entry.cachegrind?.total) { + const { total, deltas } = entry.cachegrind; + notes.push( + `Ir ${total.baseline} -> ${total.candidate} (${pct(total.candidate / Math.max(1, total.baseline))})` + + (deltas[0] + ? `, largest ${deltas[0].name} ${deltas[0].delta > 0 ? "+" : ""}${deltas[0].delta}` + : ""), + ); + } else if (entry.cachegrind?.skipped) + notes.push(`cachegrind skipped: ${entry.cachegrind.skipped}`); + for (const failure of entry.dynamicTier?.failures ?? []) notes.push(`SYMBOLS: ${failure}`); + lines.push(`${label} ${entry.id}${notes.length ? ` ${notes.join("; ")}` : ""}`); + // A build error's first line is often a warning; print all of it. + const errors = + entry.baselineError !== undefined && entry.baselineError === entry.candidateError + ? [["baseline and candidate", entry.baselineError]] + : [ + ["baseline", entry.baselineError], + ["candidate", entry.candidateError], + ].filter(([, error]) => error !== undefined); + for (const [side, error] of errors) { + lines.push(` ${side}:`); + for (const line of error.split("\n")) lines.push(` ${line}`); + } + return lines; +} + /* ── verdicts ──────────────────────────────────────────────────────────── */ /** One program's verdict from its build results and artifact comparisons. @@ -539,3 +594,142 @@ export function timeRatio(rows, resamples = 2000) { high: ratios[Math.floor(resamples * 0.975)], }; } + +/** The compile-time verdict for one group of timing rows (`bench` or + * `self`): it fails when the 95% interval lies entirely above the allowed + * growth, and is inconclusive when the interval straddles it. */ +export function timingVerdict(name, rows, maxGrowth) { + const result = timeRatio(rows); + const fails = result.low > 1 + maxGrowth; + const verdict = fails + ? "SLOWER" + : result.high > 1 + maxGrowth + ? "inconclusive (rerun with more runs)" + : "within tolerance"; + const pct = (ratio) => `${ratio >= 1 ? "+" : ""}${((ratio - 1) * 100).toFixed(2)}%`; + return { + result, + fails, + line: + `time ${name} ${rows.length > 1 ? "geomean " : ""}${pct(result.ratio)} ` + + `[95% CI ${pct(result.low)}, ${pct(result.high)}]: ${verdict}`, + failure: fails + ? `${name} compile time grew ${pct(result.ratio)} (CI low ${pct(result.low)})` + : null, + }; +} + +/** Timing rows grouped as the gate judges them. */ +export function timingGroups(rows) { + return { + bench: rows.filter((r) => r.id.startsWith("bench/")), + self: rows.filter((r) => r.id === "self/cli"), + }; +} + +/* ── baseline cache ────────────────────────────────────────────────────── */ + +/** A content hash of one artifact file, as the baseline cache records it. */ +export const artifactHash = hash; + +/** The hash of what a program's build reads, as the baseline cache records + * it: `files` (the entry and its relative imports) by path relative to + * `root` and content, read through `read`, and the optimization level. */ +export function programInputsHash(root, files, optimization, read) { + const h = createHash("sha1").update(`${optimization}\0`); + for (const file of [...files].sort()) + h.update(`${file.startsWith(root + "/") ? file.slice(root.length + 1) : file}\0`) + .update(read(file)) + .update("\0"); + return h.digest("hex"); +} + +/** The baseline cache entry recorded for program `id`, or undefined when the + * cache has none or recorded it for other inputs (`programInputsHash`): a + * program or an import that changed since the record must build its + * baseline, or a recorded build error or artifact hash of the old sources + * would settle the new sources' comparison. */ +export function recordedBaseline(cache, id, inputs) { + const entry = cache?.programs[id]; + return entry !== undefined && entry.inputs === inputs ? entry : undefined; +} + +/** The comparison a cached baseline entry settles without building the + * baseline, or null when the baseline must be built: identical artifacts + * (every recorded hash equals the candidate's) or the same build error on + * both sides. `candidate` holds the candidate's build result and `hashes` + * its artifact hashes. */ +export function cachedComparison(entry, candidate, hashes) { + if (entry === undefined) return null; + if (!entry.ok || !candidate.ok) + return !entry.ok && !candidate.ok && entry.error === candidate.error + ? { + baseline: { ok: false, error: entry.error, ms: entry.ms ?? null, cached: true }, + artifacts: {}, + } + : null; + const kinds = Object.keys(entry.hashes); + const differs = (kind) => entry.hashes[kind] === null || entry.hashes[kind] !== hashes[kind]; + if (kinds.length === 0 || kinds.some(differs)) return null; + const artifacts = {}; + for (const kind of kinds) artifacts[kind] = { identical: true }; + if (artifacts.exe) artifacts.exe.bytes = { baseline: entry.exeBytes, candidate: entry.exeBytes }; + return { baseline: { ok: true, ms: entry.ms ?? null }, artifacts }; +} + +/* ── sharded runs ──────────────────────────────────────────────────────── */ + +/** Merge the --json reports of gate shards that split one good-code set + * (--shard, --set) into a single report with one verdict. Program entries + * and their failures are concatenated; the compile-time groups and the + * "nothing was compared" check are recomputed over all shards. */ +export function mergeReports(reports) { + assert(reports.length > 0, "no gate reports to merge"); + const [first] = reports; + for (const report of reports) { + assert(report.schema === first.schema, "gate reports have different schemas"); + for (const label of ["baseline", "candidate"]) + assert( + report.trees[label].revision === first.trees[label].revision, + `gate reports compare different ${label} revisions`, + ); + } + const globals = new Set(reports.flatMap((r) => r.globalFailures ?? [])); + const merged = { + ...first, + shards: reports.length, + programs: reports.flatMap((r) => r.programs), + failures: reports.flatMap((r) => r.failures.filter((f) => !globals.has(f))), + globalFailures: [], + timing: null, + candidateSelf: reports.find((r) => r.candidateSelf)?.candidateSelf, + elapsedMs: Math.max(...reports.map((r) => r.elapsedMs ?? 0)), + }; + const ids = new Set(); + for (const p of merged.programs) { + assert(!ids.has(p.id), `program ${p.id} appears in more than one gate report`); + ids.add(p.id); + } + const timed = reports.filter((r) => r.timing !== null); + const lines = []; + if (timed.length > 0) { + const rows = timed.flatMap((r) => r.timing.rows); + const maxGrowth = timed[0].timing.maxGrowth; + merged.timing = { runs: timed[0].timing.runs, maxGrowth, rows, groups: {} }; + for (const [name, group] of Object.entries(timingGroups(rows))) { + if (group.length === 0) continue; + const { result, line, failure } = timingVerdict(name, group, maxGrowth); + merged.timing.groups[name] = result; + lines.push(line); + if (failure !== null) merged.globalFailures.push(failure); + } + } + const counts = {}; + for (const p of merged.programs) counts[p.verdict] = (counts[p.verdict] ?? 0) + 1; + if (!merged.programs.some((p) => ["identical", "allowed", "differs"].includes(p.verdict))) + merged.globalFailures.push("no program built with both checkouts, so nothing was compared"); + merged.failures.push(...merged.globalFailures); + merged.counts = counts; + merged.verdict = merged.failures.length > 0 ? "fail" : "pass"; + return { report: merged, timingLines: lines }; +} diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index 99b1bc468..b2d4c877a 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -13,6 +13,7 @@ * pattern signature) may differ; any other difference fails the gate. */ import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { existsSync, mkdirSync, @@ -23,12 +24,14 @@ import { statSync, writeFileSync, } from "node:fs"; -import { availableParallelism } from "node:os"; +import { availableParallelism, totalmem } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { readWorkloads } from "../benchmarks/runtime/workloads.mjs"; import { + artifactHash, + cachedComparison, cachegrindCounts, compareArtifact, compareCounts, @@ -38,8 +41,12 @@ import { dynamicTierSymbols, median, nmOutput, + programInputsHash, + programLines, programVerdict, - timeRatio, + recordedBaseline, + timingGroups, + timingVerdict, } from "./zero-cost-compare.mjs"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); @@ -61,6 +68,8 @@ const { values } = parseArgs({ cachegrind: { type: "boolean", default: false }, jobs: { type: "string", default: String(Math.max(1, availableParallelism())) }, json: { type: "string" }, + "baseline-cache": { type: "string" }, + "record-baseline-cache": { type: "string" }, keep: { type: "boolean", default: false }, help: { type: "boolean", default: false }, }, @@ -72,6 +81,7 @@ if (values.help || values.baseline === undefined) { [--timing-runs=N] [--self-timing-runs=N] [--max-time-growth=2] [--max-size-growth=0] [--cachegrind] [--jobs=N] [--json=] [--keep] + [--baseline-cache=] [--record-baseline-cache=] Each checkout must be built (packages/cli/dist/bootstrap.js and this host's runtime pack). Both compile the same program files, taken from --programs @@ -79,9 +89,12 @@ 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. +--shard=i/n keeps every n-th bench and corpus program; the self program runs +in shard 1. scripts/zero-cost-merge.mjs merges the shards' --json reports +into one verdict. --only filters program ids. --allow= is the change's pattern signature: programs whose source files match it may differ (reported as "allowed"). Any other difference in @@ -105,6 +118,18 @@ cachegrind (bench workloads with their arguments, corpus programs without) and compares instruction counts per function of the executable's own symbols, excluding the allocator's prefault thread. +--record-baseline-cache= builds only the baseline and records each +program's artifact hashes (or build error) in , with a hash of the +program's inputs (its entry, its relative imports and its optimization +level), then exits. +--baseline-cache= reads such a record: a program whose candidate +artifacts hash the same as the recorded baseline's is identical without a +baseline build, and the same build error on both sides is both-fail; any +other program builds the baseline as usual, and so does a program whose +inputs changed since the record. The record applies only to the +baseline revision and toolchain it was made with; otherwise it is ignored. +The self program always builds both sides, since its builds are timed. + Exit status: 0 when the gate passes, 1 when it fails.`); process.exit(values.help ? 0 : 2); } @@ -199,13 +224,7 @@ function goodCodeSet() { if (main !== undefined) entries.push(main); } else if (exts.some((ext) => name.endsWith(`.${ext}`))) entries.push(path); } - let selected = entries; - if (values.shard) { - const [index, count] = values.shard.split("/").map(Number); - assert.ok(index >= 1 && index <= count, "--shard must be i/n with 1 <= i <= n"); - selected = entries.filter((_, k) => k % count === index - 1); - } - for (const entry of selected) { + for (const entry of entries) { const head = readFileSync(entry, "utf8").split("\n", 2); const speed = head.some((line) => /^\/\/ @optimization:\s*speed\s*$/.test(line)); programs.push({ @@ -216,7 +235,17 @@ function goodCodeSet() { }); } } - return programs.filter((p) => only === null || only.test(p.id)); + const selected = programs.filter((p) => only === null || only.test(p.id)); + if (!values.shard) return selected; + const [index, count] = values.shard.split("/").map(Number); + assert.ok( + Number.isInteger(index) && Number.isInteger(count) && index >= 1 && index <= count, + "--shard must be i/n with 1 <= i <= n", + ); + // The self program takes minutes and pairs its own timing: shard 1 owns it. + const self = selected.filter((p) => p.kind === "source"); + const rest = selected.filter((p) => p.kind !== "source"); + return [...(index === 1 ? self : []), ...rest.filter((_, k) => k % count === index - 1)]; } /* ── builds ────────────────────────────────────────────────────────────── */ @@ -414,7 +443,80 @@ function revision(root) { const fmtMs = (ms) => `${(ms / 1000).toFixed(2)}s`; const pct = (ratio) => `${ratio >= 1 ? "+" : ""}${((ratio - 1) * 100).toFixed(2)}%`; -const programs = goodCodeSet(); +/* ── baseline cache ────────────────────────────────────────────────────── */ + +/** Every file under `dir`, recursively, in a stable order. */ +function filesUnder(dir) { + if (!existsSync(dir)) return []; + return readdirSync(dir, { recursive: true, withFileTypes: true }) + .filter((d) => d.isFile()) + .map((d) => join(d.parentPath, d.name)) + .sort(); +} + +/** What a baseline build depends on besides the program: the checkout's + * built compiler and CLI, its runtime packs and LLVM helpers, Node, the + * host and the program and checkout paths that reach the artifacts. */ +function toolchainFingerprint(root) { + const h = createHash("sha1"); + h.update(JSON.stringify([process.platform, process.arch, process.version, root, programsRoot])); + const clang = spawnSync("clang", ["--version"], { encoding: "utf8" }); + h.update(clang.stdout ?? ""); + // Linux links use ld.lld when the driver finds it, else the system linker. + const lld = spawnSync("clang", ["-print-prog-name=ld.lld"], { encoding: "utf8" }); + const lldPath = (lld.stdout ?? "").trim(); + h.update(lldPath); + if (lldPath.startsWith("/") && existsSync(lldPath)) + h.update(spawnSync(lldPath, ["--version"], { encoding: "utf8" }).stdout ?? ""); + const packages = join(root, "packages"); + for (const name of readdirSync(packages).sort()) { + const dir = join(packages, name); + const files = [ + ...filesUnder(join(dir, "dist")), + ...(name.startsWith("runtime-") + ? [join(dir, "runtime-pack.json"), ...filesUnder(join(dir, "artifacts"))] + : []), + ...(name.startsWith("llvm-") ? filesUnder(join(dir, "bin")) : []), + ]; + for (const file of files) + if (existsSync(file)) h.update(relative(root, file)).update(readFileSync(file)); + } + return h.digest("hex"); +} + +/** The hash of `program`'s inputs, which its baseline cache entry records. */ +function inputsHash(program) { + return programInputsHash( + programsRoot, + programInputs(program.entry), + program.kind === "source" ? "source" : (program.optimization ?? "release"), + (file) => readFileSync(file), + ); +} + +/** The content hashes of one build's artifacts (null for a missing one). */ +function buildHashes(build) { + const hashes = {}; + for (const kind of ["ir", "llvm", "exe"]) + if (kind in build) + hashes[kind] = build[kind] === null ? null : artifactHash(readFileSync(build[kind])); + return hashes; +} + +const recordPath = values["record-baseline-cache"]; +let baselineCache = null; +if (recordPath === undefined && values["baseline-cache"] !== undefined) { + const file = resolve(values["baseline-cache"]); + const cache = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null; + const fingerprint = cache === null ? null : toolchainFingerprint(trees.baseline); + if (cache?.schema === 2 && cache.fingerprint === fingerprint) baselineCache = cache; + else + console.log( + `baseline cache ${file} ${cache === null ? "is missing" : "was made with another baseline or toolchain"}; building every baseline`, + ); +} + +const programs = goodCodeSet().filter((p) => recordPath === undefined || p.kind !== "source"); assert.ok(programs.length > 0, "the selected good-code set is empty"); const report = { schema: 1, @@ -428,6 +530,13 @@ const report = { programs: [], timing: null, failures: [], + // Failures judged over the whole good-code set (compile time, nothing + // compared); scripts/zero-cost-merge.mjs recomputes them across shards. + globalFailures: [], + baselineCache: + baselineCache === null + ? null + : { programs: Object.keys(baselineCache.programs).length, used: 0 }, }; console.log( `zero-cost gate: ${programs.length} programs (${[...sets].join(", ")}), ` + @@ -436,18 +545,95 @@ console.log( let failed = false; const started = Date.now(); + +if (recordPath !== undefined) { + const record = { + schema: 2, + date: report.date, + fingerprint: toolchainFingerprint(trees.baseline), + baseline: report.trees.baseline, + programs: {}, + }; + await pool( + programs.map((program) => ({ + run: async () => { + const build = await buildProgram(program, "baseline"); + const inputs = inputsHash(program); + record.programs[program.id] = build.ok + ? { + inputs, + ok: true, + ms: build.ms, + hashes: buildHashes(build), + ...(build.exe ? { exeBytes: statSync(build.exe).size } : {}), + } + : { inputs, ok: false, ms: build.ms ?? null, error: build.error }; + if (build.dir) rmSync(build.dir, { recursive: true, force: true }); + }, + })), + ); + mkdirSync(dirname(resolve(recordPath)), { recursive: true }); + writeFileSync(recordPath, JSON.stringify(record) + "\n"); + rmSync(work, { recursive: true, force: true }); + const failures = Object.values(record.programs).filter((p) => !p.ok).length; + console.log( + `zero-cost gate: recorded ${programs.length} baseline programs (${failures} build errors) ` + + `in ${recordPath} in ${fmtMs(Date.now() - started)}`, + ); + process.exit(0); +} + +/** The candidate's own compiler source must still build with the candidate + * (see below). On a host with room for a third self-hosted build it runs + * alongside the comparison builds instead of after them. */ +const checkCandidateSelf = + sets.has("self") && programsRoot !== trees.candidate && programs.some((p) => p.kind === "source"); +const buildCandidateSelf = () => + buildProgram( + { + id: "self/candidate-source", + entry: join(trees.candidate, "packages/compiler/src/native/cli.ts"), + kind: "source", + }, + "candidate", + ); +const earlyCandidateSelf = + checkCandidateSelf && totalmem() >= 40 * 1024 ** 3 ? buildCandidateSelf() : null; + await pool( programs.map((program) => ({ heavy: program.kind === "source", run: async () => { - const [baseline, candidate] = await Promise.all([ - buildProgram(program, "baseline"), - buildProgram(program, "candidate"), - ]); + const recorded = + program.kind === "source" || baselineCache === null + ? undefined + : recordedBaseline(baselineCache, program.id, inputsHash(program)); + let baseline; + let candidate; + let settled = null; + if (recorded === undefined) + [baseline, candidate] = await Promise.all([ + buildProgram(program, "baseline"), + buildProgram(program, "candidate"), + ]); + else { + candidate = await buildProgram(program, "candidate"); + settled = cachedComparison(recorded, candidate, candidate.ok ? buildHashes(candidate) : {}); + // A cached identical baseline has the candidate's artifacts. + baseline = + settled === null + ? await buildProgram(program, "baseline") + : settled.baseline.ok + ? { ...candidate, ms: settled.baseline.ms, cached: true } + : settled.baseline; + if (settled !== null) report.baselineCache.used++; + } const allowed = allow !== null && programInputs(program.entry).some((f) => allow.test(readFileSync(f, "utf8"))); - const artifacts = baseline.ok && candidate.ok ? compareBuilds(baseline, candidate) : {}; + const artifacts = + settled?.artifacts ?? + (baseline.ok && candidate.ok ? compareBuilds(baseline, candidate) : {}); // The benchmark workloads and the self-hosted compiler always build. const required = !program.id.startsWith("corpus/"); const { verdict, fails } = programVerdict({ @@ -464,6 +650,7 @@ await pool( buildMs: { baseline: baseline.ms ?? null, candidate: candidate.ms ?? null }, artifacts, }; + if (settled !== null) entry.cachedBaseline = true; if (!baseline.ok) entry.baselineError = baseline.error; if (!candidate.ok) entry.candidateError = candidate.error; const size = artifacts.exe?.bytes; @@ -505,7 +692,8 @@ await pool( nmCache.delete(baseline.exe); nmCache.delete(candidate.exe); if (!values.keep) { - if (baseline.dir) rmSync(baseline.dir, { recursive: true, force: true }); + if (baseline.dir && !baseline.cached) + rmSync(baseline.dir, { recursive: true, force: true }); if (candidate.dir) rmSync(candidate.dir, { recursive: true, force: true }); } }, @@ -513,50 +701,7 @@ await pool( ); function printProgram(entry) { - const label = { - identical: "identical", - allowed: "allowed ", - differs: "DIFFERS ", - "build-error": "ERROR ", - "both-fail": "both-fail", - }[entry.verdict]; - const notes = []; - for (const [kind, result] of Object.entries(entry.artifacts)) - if (!result.identical) { - const counts = result.changed - ? ` (${result.changed.length} changed, ${result.added.length} added, ${result.removed.length} removed)` - : ""; - notes.push(`${kind}: ${result.first}${counts}`); - } - const size = entry.artifacts.exe?.bytes; - if (size && size.baseline !== size.candidate) - notes.push( - `size ${size.baseline} -> ${size.candidate} B (${pct(size.candidate / size.baseline)})`, - ); - if (entry.cachegrind?.total) { - const { total, deltas } = entry.cachegrind; - notes.push( - `Ir ${total.baseline} -> ${total.candidate} (${pct(total.candidate / Math.max(1, total.baseline))})` + - (deltas[0] - ? `, largest ${deltas[0].name} ${deltas[0].delta > 0 ? "+" : ""}${deltas[0].delta}` - : ""), - ); - } else if (entry.cachegrind?.skipped) - notes.push(`cachegrind skipped: ${entry.cachegrind.skipped}`); - for (const failure of entry.dynamicTier?.failures ?? []) notes.push(`SYMBOLS: ${failure}`); - console.log(`${label} ${entry.id}${notes.length ? ` ${notes.join("; ")}` : ""}`); - // A build error's first line is often a warning; print all of it. - const errors = - entry.baselineError !== undefined && entry.baselineError === entry.candidateError - ? [["baseline and candidate", entry.baselineError]] - : [ - ["baseline", entry.baselineError], - ["candidate", entry.candidateError], - ].filter(([, error]) => error !== undefined); - for (const [side, error] of errors) { - console.log(` ${side}:`); - for (const line of error.split("\n")) console.log(` ${line}`); - } + for (const line of programLines(entry)) console.log(line); } /* Compile time: paired --emit=llvm builds, the baseline and the candidate @@ -602,38 +747,46 @@ if (timingRuns > 0) { ); } report.timing = { runs: timingRuns, maxGrowth: maxTimeGrowth, rows, groups: {} }; - const groups = { - bench: rows.filter((r) => r.id.startsWith("bench/")), - self: rows.filter((r) => r.id === "self/cli"), - }; - for (const [name, group] of Object.entries(groups)) { + for (const [name, group] of Object.entries(timingGroups(rows))) { if (group.length === 0) continue; - const result = timeRatio(group); + const { result, line, failure } = timingVerdict(name, group, maxTimeGrowth); report.timing.groups[name] = result; - const verdict = - result.low > 1 + maxTimeGrowth - ? "SLOWER" - : result.high > 1 + maxTimeGrowth - ? "inconclusive (rerun with more runs)" - : "within tolerance"; - console.log( - `time ${name} ${group.length > 1 ? "geomean " : ""}${pct(result.ratio)} ` + - `[95% CI ${pct(result.low)}, ${pct(result.high)}]: ${verdict}`, - ); - if (result.low > 1 + maxTimeGrowth) { + console.log(line); + if (failure !== null) { failed = true; - report.failures.push( - `${name} compile time grew ${pct(result.ratio)} (CI low ${pct(result.low)})`, - ); + report.failures.push(failure); + report.globalFailures.push(failure); } } } +// 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 (checkCandidateSelf) { + const result = await (earlyCandidateSelf ?? buildCandidateSelf()); + 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))) { failed = true; - report.failures.push("no program built with both checkouts, so nothing was compared"); + const failure = "no program built with both checkouts, so nothing was compared"; + report.failures.push(failure); + report.globalFailures.push(failure); } report.counts = counts; report.verdict = failed ? "fail" : "pass"; @@ -647,7 +800,8 @@ else console.log(`artifacts kept in ${work}`); console.log( `zero-cost gate: ${failed ? "FAIL" : "PASS"} — ${Object.entries(counts) .map(([k, v]) => `${v} ${k}`) - .join(", ")} in ${fmtMs(report.elapsedMs)}`, + .join(", ")} in ${fmtMs(report.elapsedMs)}` + + (report.baselineCache ? ` (${report.baselineCache.used} baselines from the cache)` : ""), ); for (const failure of report.failures) console.log(` ${failure}`); process.exit(failed ? 1 : 0); diff --git a/scripts/zero-cost-merge.mjs b/scripts/zero-cost-merge.mjs new file mode 100644 index 000000000..d32022dd9 --- /dev/null +++ b/scripts/zero-cost-merge.mjs @@ -0,0 +1,52 @@ +/* Merge zero-cost gate shards into one verdict. + * + * A sharded gate runs scripts/zero-cost-gate.mjs several times over one + * good-code set (--shard=i/n for the bench and corpus programs, the self + * program in shard 1 or its own --set=self run), each with --json. This + * script merges those reports: program verdicts and failures are + * concatenated, and the compile-time verdicts and the "nothing was + * compared" check are recomputed over every shard's rows. */ +import { readFileSync, writeFileSync } from "node:fs"; +import { parseArgs } from "node:util"; +import { mergeReports, programLines } from "./zero-cost-compare.mjs"; + +const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { + json: { type: "string" }, + quiet: { type: "boolean", default: false }, + help: { type: "boolean", default: false }, + }, +}); +if (values.help || positionals.length === 0) { + console.log(`Usage: node scripts/zero-cost-merge.mjs [--json=] [--quiet] ... + +Merges the --json reports of zero-cost gate shards and prints one summary: +every program that is not identical, the compile-time verdicts and the +failures. --quiet prints only the summary and failures. --json writes the +merged report. Exit status: 0 when the merged gate passes, 1 when it fails.`); + process.exit(values.help ? 0 : 2); +} + +const reports = positionals.map((file) => JSON.parse(readFileSync(file, "utf8"))); +const { report, timingLines } = mergeReports(reports); +if (!values.quiet) + for (const entry of report.programs) + if (entry.verdict !== "identical") for (const line of programLines(entry)) console.log(line); +for (const line of timingLines) console.log(line); +if (report.candidateSelf) + console.log( + report.candidateSelf.ok + ? `self candidate compiler source builds with the candidate (${(report.candidateSelf.buildMs / 1000).toFixed(2)}s)` + : "self candidate compiler source FAILS to build with the candidate", + ); +const cached = report.programs.filter((p) => p.cachedBaseline).length; +console.log( + `zero-cost gate: ${report.verdict === "pass" ? "PASS" : "FAIL"} — ${Object.entries(report.counts) + .map(([k, v]) => `${v} ${k}`) + .join(", ")} over ${report.shards} shards (longest ${(report.elapsedMs / 1000).toFixed(2)}s` + + `${cached ? `, ${cached} baselines from the cache` : ""})`, +); +for (const failure of report.failures) console.log(` ${failure}`); +if (values.json) writeFileSync(values.json, JSON.stringify(report, null, 2) + "\n"); +process.exit(report.verdict === "pass" ? 0 : 1); 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/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/7393-non-null-element-operands.ts b/tests/corpus/7393-non-null-element-operands.ts new file mode 100644 index 000000000..0fd7190a3 --- /dev/null +++ b/tests/corpus/7393-non-null-element-operands.ts @@ -0,0 +1,81 @@ +// Non-null assertions on array element reads whose index is out of bounds +// or names a hole, used as operands of operators that convert them. `!` +// performs no runtime check, so the read is undefined, as in Node: NaN in +// arithmetic, false in comparisons, "undefined" in strings. + +const values = [3, 1, 4, 1, 5]; +const words = ["alpha", "beta"]; +const flags = [true, false]; +const holey: number[] = []; +holey[3] = 7; + +function show(v: unknown): string { + return Object.is(v, -0) ? "-0" : String(v); +} + +function line(label: string, parts: unknown[]): void { + console.log(label.padEnd(10), parts.map(show).join(" ")); +} + +for (const k of [0, 4, 5, -1, 1.5, 100]) { + line(`k=${k}`, [ + values[k]! + 1, + values[k]! * 2, + -values[k]!, + values[k]! | 0, + values[k]! < 3, + values[k]! >= 1, + values[k]! === 1, + values[k]! !== 1, + `${values[k]!}`, + "v=" + values[k]!, + Math.max(values[k]!, 0), + words[k]! + "!", + `<${words[k]!}>`, + words[k]! < "b", + !flags[k]!, + ]); +} + +line("holes", [holey[0]! + 1, holey[3]! + 1, `${holey[1]!}`, holey[2]! === undefined, holey[9]! > 0]); + +// `+` with an untyped partner concatenates or adds when it runs. +const untypedText: any = "x"; +const untypedNumber: any = 5; +for (const k of [1, 9]) { + line(`untyped ${k}`, [ + values[k]! + untypedText, + untypedText + values[k]!, + values[k]! + untypedNumber, + words[k]! + untypedNumber, + words[k]! + untypedText, + ]); +} + +let sum = 0; +let text = ""; +for (let i = 0; i <= values.length; i++) { + sum += values[i]!; + text += words[i % 3]!; + text += ","; +} +line("compound", [sum, text]); + +// A counter array: an index past the end reads NaN, and the store widens +// the array to doubles. +function counter(n: number, m: number): number[] { + const a = new Array(n).fill(0) as number[]; + for (let i = 0; i < 6000; i++) a[i % m] = a[i % m]! + 1; + return [a[0]!, a[m - 1]!, a.length, a[n + 1]! + 0]; +} +line("counter", counter(5000, 5000)); +line("past", counter(5000, 5003)); +line("zero", counter(4, 0)); + +const grid = [ + [1, 2], + [3, 4], +]; +let total = 0; +for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) total += grid[r % 2]![c]! * 1; +line("grid", [total]); diff --git a/tests/corpus/7395-non-null-element-dereference.ts b/tests/corpus/7395-non-null-element-dereference.ts new file mode 100644 index 000000000..67d9249d1 --- /dev/null +++ b/tests/corpus/7395-non-null-element-dereference.ts @@ -0,0 +1,142 @@ +// Member reads and writes through non-null asserted array element reads. +// `!` performs no runtime check: a missing element is undefined, so the +// member access throws Node's TypeError, which the program can catch. + +class Cell { + count = 0; + label: string; + constructor(label: string) { + this.label = label; + } + bump(): number { + return ++this.count; + } +} + +function cell(label: string): Cell { + return new Cell(label); +} + +const cells = [cell("a"), cell("b")]; +const words = ["alpha", "beta"]; +const grid = [ + [1, 2], + [3, 4], +]; +const maps = [new Map([["x", 1]])]; +const holey: Cell[] = []; +holey[2] = cell("z"); +let reads = 0; +function at(n: number): number { + reads++; + return n; +} + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label.padEnd(12), "ok", f()); + } catch (e) { + const error = e as Error; + console.log(label.padEnd(12), error instanceof TypeError, error.message); + } +} + +for (const k of [0, 1, 2, -1]) { + attempt(`label ${k}`, () => cells[k]!.label); + attempt(`bump ${k}`, () => cells[k]!.bump()); + attempt(`set ${k}`, () => (cells[k]!.label = "set")); + attempt(`count++ ${k}`, () => cells[k]!.count++); + attempt(`length ${k}`, () => words[k]!.length); + attempt(`upper ${k}`, () => words[k]!.toUpperCase()); + attempt(`grid ${k}`, () => grid[k]![0]); + attempt(`row ${k}`, () => { + const j = 1; + return grid[k]![j]; + }); + attempt(`push ${k}`, () => grid[k]!.push(9)); + attempt(`map ${k}`, () => maps[k]!.get("x")); +} + +attempt("hole", () => holey[0]!.label); +attempt("hole value", () => holey[2]!.label); +attempt("order", () => cells[at(7)]!.count + at(1)); +console.log("reads", reads); + +let total = 0; +try { + for (let i = 0; i < 4; i++) total += grid[i]![0]!; +} catch (e) { + console.log("loop", (e as Error).message, total); +} +console.log(cells.map((c) => `${c.label}:${c.count}`).join(" ")); + +// A write through a missing element evaluates the key and the value before +// it throws; an error the value throws is the one that propagates. +const events: string[] = []; +const note = (event: string, value: number): number => { + events.push(event); + return value; +}; +attempt("write order", () => { + cells[note("index", 7)]!.count = note("value", 1); +}); +attempt("write key order", () => { + grid[note("row", 9)]![note("column", 0)] = note("cell", 5); +}); +attempt("write literal key", () => { + grid[9]![0] = note("cell 0", 5); +}); +attempt("value throws", () => { + cells[7]!.count = (() => { + throw new Error("from the value"); + })(); +}); +attempt("present write", () => { + cells[0]!.count = note("present", 41) + 1; + return cells[0]!.count; +}); +console.log("events", events.join(",")); + +// The array is read before an index that replaces it. +let current = [new Cell("first")]; +const replace = (): number => { + current = []; + return 0; +}; +attempt("replaced read", () => current[replace()]!.label); +current = [new Cell("second")]; +attempt("replaced write", () => { + current[replace()]!.label = "written"; +}); +let names = ["kept"]; +attempt("replaced string", () => `${names[(names = [], 0)]!}`); +console.log(current.length, names.length); + +// The element is read before the key and the value: a value that replaces, +// removes or adds it writes to, or misses, the element the target read, and +// a key the value reassigns is the one read before it. +const slots = [new Cell("p"), new Cell("q")]; +const original = slots[0]!; +attempt("value replaces", () => { + slots[0]!.label = ((slots[0] = new Cell("r")), "set"); + return `${original.label} ${slots[0]!.label}`; +}); +const tail = slots[1]!; +attempt("value removes", () => { + slots[1]!.label = (slots.pop(), "gone"); + return `${tail.label} ${slots.length}`; +}); +attempt("value adds", () => { + slots[1]!.label = (slots.push(new Cell("s")), "added"); +}); +const table: number[][] = [[1, 2]]; +let column = 0; +attempt("value moves key", () => { + table[0]![column] = ((column = 1), 5); + return table[0]!.join(","); +}); +column = 0; +attempt("value moves missing key", () => { + table[4]![column] = ((column = 1), 5); +}); +console.log(slots.length, column); diff --git a/tests/corpus/7515-net-socket-backpressure.ts b/tests/corpus/7515-net-socket-backpressure.ts index 260726a15..6524e6d54 100644 --- a/tests/corpus/7515-net-socket-backpressure.ts +++ b/tests/corpus/7515-net-socket-backpressure.ts @@ -7,6 +7,11 @@ // 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"; @@ -113,6 +118,15 @@ function afterReset(port: number): Promise { 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(); @@ -120,6 +134,11 @@ const server = createServer((socket: Socket) => { socket.pause(); paused = socket; } else socket.destroy(); + if (accepted !== null) { + const resolve = accepted; + accepted = null; + resolve(); + } }); server.listen(0, "127.0.0.1", async () => { @@ -127,7 +146,9 @@ server.listen(0, "127.0.0.1", async () => { 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"; 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/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/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/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/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/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/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/8370-stack-overflow-cycles/main.ts b/tests/corpus/8370-stack-overflow-cycles/main.ts new file mode 100644 index 000000000..3b9ef5ddc --- /dev/null +++ b/tests/corpus/8370-stack-overflow-cycles/main.ts @@ -0,0 +1,69 @@ +// Unbounded recursion through each kind of call cycle raises Node's +// RangeError, and the program recovers and recurses again afterwards. +import { Even, Odd, Tree } from "./shapes.ts"; + +function probe(name: string, run: (n: number) => number): void { + try { + console.log(name, "returned", run(10_000_000)); + } catch (e) { + console.log(name, e instanceof RangeError, (e as Error).message); + } + console.log(name, "after", run(100)); +} + +function down(n: number): number { + return n === 0 ? 0 : 1 + down(n - 1); +} +probe("direct", down); + +function ping(n: number): number { + return n === 0 ? 0 : 1 + pong(n - 1); +} +function pong(n: number): number { + return n === 0 ? 0 : 2 + ping(n - 1); +} +probe("mutual", ping); + +function first(n: number): number { + return n === 0 ? 0 : 1 + second(n - 1); +} +function second(n: number): number { + return n === 0 ? 0 : 1 + third(n - 1); +} +function third(n: number): number { + return n === 0 ? 0 : 1 + fourth(n - 1); +} +function fourth(n: number): number { + return n === 0 ? 0 : 1 + fifth(n - 1); +} +function fifth(n: number): number { + return n === 0 ? 0 : 1 + first(n - 1); +} +probe("ring", first); + +function viaValue(n: number, again: (m: number) => number): number { + return n === 0 ? 0 : 1 + again(n - 1); +} +const callback = (m: number): number => viaValue(m, callback); +probe("closure", callback); + +function viaMap(n: number): number { + return n === 0 ? 0 : 1 + [n - 1].map(viaMap)[0]!; +} +probe("library callback", viaMap); + +const even = new Even(); +const odd = new Odd(); +even.next = odd; +odd.next = even; +probe("virtual", (n) => even.depth(n)); + +probe("constructor", (n) => { + let tree: Tree | null = new Tree(n); + let count = 0; + while (tree !== null) { + count++; + tree = tree.child; + } + return count; +}); diff --git a/tests/corpus/8370-stack-overflow-cycles/shapes.ts b/tests/corpus/8370-stack-overflow-cycles/shapes.ts new file mode 100644 index 000000000..6f5f6fada --- /dev/null +++ b/tests/corpus/8370-stack-overflow-cycles/shapes.ts @@ -0,0 +1,24 @@ +// Classes in their own module, so their names carry the module prefix. +export abstract class Link { + next: Link | null = null; + abstract depth(n: number): number; +} + +export class Even extends Link { + depth(n: number): number { + return n === 0 ? 0 : 1 + this.next!.depth(n - 1); + } +} + +export class Odd extends Link { + depth(n: number): number { + return n === 0 ? 0 : 2 + this.next!.depth(n - 1); + } +} + +export class Tree { + child: Tree | null; + constructor(n: number) { + this.child = n === 0 ? null : new Tree(n - 1); + } +} diff --git a/tests/corpus/8380-untyped-generator-loops.mjs b/tests/corpus/8380-untyped-generator-loops.mjs new file mode 100644 index 000000000..a6dd20ebe --- /dev/null +++ b/tests/corpus/8380-untyped-generator-loops.mjs @@ -0,0 +1,162 @@ +// Untyped for-of and for await loops over generator objects: values, +// completion values, early exits, errors, other iterables through the same +// loop, and the microtask order of each step. +function* numbers(n) { + for (let i = 0; i < n; i++) yield i; + return "done"; +} +function* mixed() { + yield "a"; + yield { k: 1 }; + yield undefined; + yield [1, 2]; + yield Promise.resolve(7); + return 99; +} +async function* asyncNumbers(n) { + for (let i = 0; i < n; i++) yield i * 10; + return "async done"; +} +async function* asyncMixed() { + yield "x"; + yield Promise.resolve(8); + yield null; +} + +function collect(it) { + const out = []; + for (const x of it) out.push(x); + return out; +} +async function collectAsync(it) { + const out = []; + for await (const x of it) out.push(x); + return out; +} +const show = (v) => JSON.stringify(v, (k, x) => (x instanceof Promise ? "promise" : x === undefined ? "undef" : x)); + +async function main() { + // The same loops over generators, arrays, Maps, strings and custom iterators. + console.log("sync", show(collect(numbers(4))), show(collect(mixed()))); + console.log("sync other", show(collect([1, 2])), show(collect(new Map([[1, 2]]))), show(collect("hé"))); + const custom = { + [Symbol.iterator]() { + let i = 0; + return { next: () => ({ value: i * i, done: i++ > 2 }) }; + }, + }; + console.log("sync custom", show(collect(custom))); + console.log("async", show(await collectAsync(asyncNumbers(3))), show(await collectAsync(asyncMixed()))); + console.log("async over sync", show(await collectAsync(mixed())), show(await collectAsync(numbers(3)))); + console.log("async other", show(await collectAsync([Promise.resolve(1), 2])), show(await collectAsync(custom))); + const asyncCustom = { + [Symbol.asyncIterator]() { + let i = 0; + return { next: async () => ({ value: `c${i}`, done: i++ > 1 }) }; + }, + }; + console.log("async custom", show(await collectAsync(asyncCustom))); + // Symbol.asyncIterator returning a sync generator: each result is awaited, + // its value is not, so a yielded promise reaches the loop as a promise. + const viaAsyncIterator = { [Symbol.asyncIterator]: () => mixed() }; + const seen = []; + for await (const x of viaAsyncIterator) seen.push(x instanceof Promise ? `promise ${await x}` : x); + console.log("async iterator sync", show(seen)); + + // A generator that has started keeps its position; one that finished stays done. + const started = numbers(5); + started.next(); + console.log("started", show(collect(started)), show(started.next())); + + // Early exits close the generator: finally blocks run once. + function* guarded(label) { + try { + yield 1; + yield 2; + } finally { + console.log(label, "finally"); + } + } + const pick = (v) => v; + for (const x of pick(guarded("break"))) { + console.log("break at", x); + break; + } + try { + for (const x of pick(guarded("throw"))) throw new Error(`body ${x}`); + } catch (e) { + console.log("caught", e.message); + } + const g = pick(guarded("after")); + for (const x of g) { + if (x === 1) break; + } + console.log("after break", show(g.next())); + async function* aguarded(label) { + try { + yield 1; + yield 2; + } finally { + console.log(label, "finally"); + } + } + for await (const x of pick(aguarded("abreak"))) { + console.log("abreak at", x); + break; + } + for await (const x of pick(guarded("sync abreak"))) { + console.log("sync abreak at", x); + break; + } + + // Errors from the generator body reach the loop. + function* failing() { + yield 1; + throw new TypeError("generator failed"); + } + async function* afailing() { + yield 1; + throw new RangeError("async generator failed"); + } + try { + for (const x of pick(failing())) console.log("failing got", x); + } catch (e) { + console.log("caught", e.name, e.message); + } + try { + for await (const x of pick(afailing())) console.log("afailing got", x); + } catch (e) { + console.log("caught", e.name, e.message); + } + // Microtask order: each step against a promise chain ticking alongside. + const ticker = (label, n) => { + let p = Promise.resolve(); + for (let i = 0; i < n; i++) p = p.then(() => console.log(label, "tick", i)); + return p; + }; + const consume = async (label, it) => { + for await (const x of it) console.log(label, "value", x); + console.log(label, "end"); + }; + await Promise.all([consume("sync", pick(numbers(3))), ticker("sync", 10)]); + await Promise.all([consume("async", pick(asyncNumbers(3))), ticker("async", 10)]); + await Promise.all([consume("promises", pick(mixed())), ticker("promises", 14)]); + + // Stages chained through untyped parameters. + async function* double(src) { + for await (const x of src) yield x * 2; + } + async function* keepOdd(src) { + for await (const x of src) if (x % 4 !== 0) yield x; + } + let sum = 0; + for await (const x of keepOdd(double(asyncNumbers(50)))) sum += x; + console.log("stages", sum); + let syncSum = 0; + function* inc(src) { + for (const x of src) yield x + 1; + } + for (const x of inc(inc(numbers(100)))) syncSum += x; + console.log("sync stages", syncSum); +} +main(); diff --git a/tests/corpus/8381-untyped-generator-stages.mjs b/tests/corpus/8381-untyped-generator-stages.mjs new file mode 100644 index 000000000..d326a00c1 --- /dev/null +++ b/tests/corpus/8381-untyped-generator-stages.mjs @@ -0,0 +1,82 @@ +// Untyped stages chained through generator objects: a sync generator source +// consumed with for await, async generator transforms, an async function +// consumer, and the same stages over arrays and custom iterables. +function run(source, transform, destination) { + return destination(transform(source())); +} +const N = 2000; +const total = await run( + function* () { + for (let i = 0; i < N; i++) yield i; + }, + async function* (src) { + for await (const n of src) yield n * 2; + }, + async function (src) { + let sum = 0; + for await (const n of src) sum += n; + return sum; + }, +); +console.log("total", total); + +async function* map(it, f) { + for await (const x of it) yield await f(x); +} +async function* take(it, n) { + if (n <= 0) return; + for await (const x of it) { + yield x; + if (--n === 0) return; + } +} +async function* source(n) { + try { + for (let i = 0; i < n; i++) yield i; + } finally { + console.log("source closed"); + } +} +async function toArray(it) { + const out = []; + for await (const x of it) out.push(x); + return out; +} +const half = async (x) => x / 2; +console.log(await toArray(take(map(source(10), half), 3))); +console.log(await toArray(map([1, 2, 3], half))); +console.log(await toArray(map(new Set(["a", "b"]), async (s) => s.toUpperCase()))); +const words = { + *[Symbol.iterator]() { + yield "x"; + yield "y"; + }, +}; +console.log(await toArray(map(words, async (s) => s + s))); + +// A stage that switches iterables between calls of the same loop. +function* letters() { + yield "p"; + yield "q"; +} +async function* numbersAsync() { + yield 1; + yield 2; +} +const sources = [letters(), numbersAsync(), ["z"], letters()]; +for (const s of sources) console.log(await toArray(s)); + +// Sync stages: generator to generator through untyped parameters. +function* inc(src) { + for (const x of src) yield x + 1; +} +function* evens(src) { + for (const x of src) if (x % 2 === 0) yield x; +} +function* range(n) { + for (let i = 0; i < n; i++) yield i; + return "range done"; +} +let s = 0; +for (const x of evens(inc(range(1000)))) s += x; +console.log("sync", s, [...inc([10, 20])], Array.from(evens(range(7)))); diff --git a/tests/corpus/8382-stackless-async-generator-steps.mjs b/tests/corpus/8382-stackless-async-generator-steps.mjs new file mode 100644 index 000000000..507040de0 --- /dev/null +++ b/tests/corpus/8382-stackless-async-generator-steps.mjs @@ -0,0 +1,172 @@ +// Async generators whose bodies suspend only at their own awaits and yields: +// requests queued while the body awaits, return() and throw() at a yield and +// during an await, finally blocks that await, yield* into another async +// generator, awaits of plain values and of settled and pending promises, a +// deep chain of stages, and the microtask order around them. +const log = []; +const tick = (label) => Promise.resolve().then(() => log.push(label)); +const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 1)); + +async function* counter(n) { + try { + for (let i = 0; i < n; i++) { + const v = await (i % 2 === 0 ? i : Promise.resolve(i)); + log.push(`yield ${v}`); + yield v; + } + return "done"; + } finally { + log.push("counter finally"); + await later(0); + log.push("counter finally after await"); + } +} + +// Requests queued before the body settles the first one. +{ + const it = counter(3); + const requests = []; + for (let i = 0; i < 5; i++) requests.push(it.next()); + const results = []; + for (const request of requests) results.push(await request); + console.log(JSON.stringify(results)); + console.log(log.splice(0).join(" | ")); +} + +// return() at a yield runs the finally block, which awaits. +{ + const it = counter(5); + console.log(JSON.stringify(await it.next())); + const ret = it.return("early"); + tick("tick after return()"); + console.log(JSON.stringify(await ret)); + console.log(JSON.stringify(await it.next())); + console.log(log.splice(0).join(" | ")); +} + +// return() queued while the body awaits a pending promise. +async function* slow() { + try { + const v = await later(7); + yield v; + yield v + 1; + return "slow done"; + } finally { + log.push("slow finally"); + } +} +{ + const it = slow(); + const first = it.next(); + const ret = it.return("stopped"); + console.log(JSON.stringify(await first), JSON.stringify(await ret)); + console.log(log.splice(0).join(" | ")); +} + +// throw() at a yield, caught by the body, which keeps yielding. +async function* catcher() { + for (let i = 0; ; i++) { + try { + yield i; + } catch (e) { + log.push(`caught ${e}`); + await null; + yield `recovered ${i}`; + return; + } + } +} +{ + const it = catcher(); + console.log(JSON.stringify(await it.next())); + console.log(JSON.stringify(await it.throw("boom"))); + console.log(JSON.stringify(await it.next())); + console.log(log.splice(0).join(" | ")); +} + +// A body that throws after an await rejects its request and completes. +async function* failing() { + yield 1; + await later(0); + throw new Error("failed after await"); +} +{ + const it = failing(); + console.log(JSON.stringify(await it.next())); + try { + await it.next(); + } catch (e) { + console.log("rejected:", e.message); + } + console.log(JSON.stringify(await it.next())); +} + +// yield* into another async generator, and break out of for await. +async function* inner() { + try { + yield "a"; + yield await later("b"); + yield "c"; + } finally { + log.push("inner finally"); + } +} +async function* outer() { + yield "start"; + const r = yield* inner(); + log.push(`inner returned ${r}`); + yield "end"; +} +{ + const seen = []; + for await (const x of outer()) { + seen.push(x); + if (x === "b") break; + } + console.log(seen.join(",")); + console.log(log.splice(0).join(" | ")); +} + +// Stages chained through untyped values, interleaved with a promise chain. +async function* source(n) { + for (let i = 0; i < n; i++) yield i; +} +async function* map(it, f) { + for await (const x of it) yield await f(x); +} +async function* filter(it, p) { + for await (const x of it) if (p(x)) yield x; +} +{ + const order = []; + const chain = (async () => { + for await (const x of filter(map(source(6), async (x) => x * 3), (v) => v % 2 === 0)) order.push(`item ${x}`); + })(); + Promise.resolve() + .then(() => order.push("p1")) + .then(() => order.push("p2")) + .then(() => order.push("p3")); + await chain; + console.log(order.join(" ")); +} + +// A deep chain of pass-through stages. +async function* pass(it) { + for await (const x of it) yield x; +} +function stack(depth) { + return depth === 0 ? source(4) : pass(stack(depth - 1)); +} +{ + let sum = 0; + for await (const x of stack(500)) sum += x; + console.log("deep sum", sum); +} + +// Generator objects keep their tag and protocol. +{ + const g = counter(1); + console.log(Object.prototype.toString.call(g)); + console.log(JSON.stringify(await g.next()), JSON.stringify(await g.next())); + log.splice(0); +} diff --git a/tests/corpus/8383-stackless-async-generator-steps.ts b/tests/corpus/8383-stackless-async-generator-steps.ts new file mode 100644 index 000000000..f07e1d0f7 --- /dev/null +++ b/tests/corpus/8383-stackless-async-generator-steps.ts @@ -0,0 +1,99 @@ +// Typed async generators whose bodies suspend only at their own awaits and +// yields: number, string and object channels, values passed into next(), +// return() and throw() while the body awaits, finally blocks that await, +// yield* delegation, and an async function consuming them. +const log: string[] = []; +const later = (value: T): Promise => new Promise((resolve) => setTimeout(() => resolve(value), 1)); + +async function* numbers(n: number): AsyncGenerator { + try { + for (let i = 0; i < n; i++) { + yield i % 2 === 0 ? i : await later(i); + } + return "numbers done"; + } finally { + await later(0); + log.push("numbers finally"); + } +} + +async function* accumulate(): AsyncGenerator { + let total = 0; + for (;;) { + const v = yield total; + if (v < 0) return total; + total += await Promise.resolve(v); + } +} + +interface Point { + x: number; + y: number; +} +async function* points(n: number): AsyncGenerator { + for (let i = 0; i < n; i++) { + await null; + yield { x: i, y: i * i }; + } +} + +async function* labels(): AsyncGenerator { + yield "first"; + yield* (async function* () { + yield "inner one"; + yield await later("inner two"); + })(); + yield "last"; +} + +async function sum(it: AsyncGenerator): Promise { + let s = 0; + for await (const v of it) s += v; + return s; +} + +async function main(): Promise { + console.log("sum", await sum(numbers(6))); + console.log(log.splice(0).join(" | ")); + + const acc = accumulate(); + console.log(JSON.stringify(await acc.next(0))); + for (const v of [1, 2, 3]) console.log(JSON.stringify(await acc.next(v))); + console.log(JSON.stringify(await acc.next(-1))); + console.log(JSON.stringify(await acc.next(5))); + + for await (const p of points(3)) console.log(p.x, p.y); + + const words: string[] = []; + for await (const w of labels()) words.push(w); + console.log(words.join(", ")); + + const it = numbers(10); + console.log(JSON.stringify(await it.next())); + const pending = it.next(); + const ret = it.return("returned"); + console.log(JSON.stringify(await pending), JSON.stringify(await ret)); + console.log(log.splice(0).join(" | ")); + + const thrower = numbers(4); + await thrower.next(); + try { + await thrower.throw(new Error("thrown in")); + } catch (e) { + console.log("rejected:", (e as Error).message); + } + console.log(JSON.stringify(await thrower.next())); + console.log(log.splice(0).join(" | ")); + + const order: string[] = []; + const run = (async () => { + for await (const v of numbers(3)) order.push(`v${v}`); + })(); + void Promise.resolve() + .then(() => order.push("m1")) + .then(() => order.push("m2")); + await run; + console.log(order.join(" ")); + log.splice(0); +} +await main(); 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/8405-zlib-compressed-bytes.ts b/tests/corpus/8405-zlib-compressed-bytes.ts new file mode 100644 index 000000000..bfada812d --- /dev/null +++ b/tests/corpus/8405-zlib-compressed-bytes.ts @@ -0,0 +1,96 @@ +// Compressed zlib bytes over the vendored zlib, the copy Node bundles: deflate, +// raw deflate and gzip output match Node's for every level, strategy, memLevel, +// windowBits and dictionary, for text, binary and a 1 MiB input that slides the +// window, and for streams that flush and change parameters between writes. +import { createHash } from "node:crypto"; +import { + constants, + createDeflate, + createGzip, + deflateRawSync, + deflateSync, + gunzipSync, + gzipSync, + inflateRawSync, + inflateSync, + type Deflate, + type Gzip, + type ZlibOptions, +} from "node:zlib"; + +const digest = (bytes: Buffer): string => + `${bytes.length} ${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`; + +const words = ["stream", "pipeline", "émetteur", "π", "buffer", "deflate", "\u0000", "window"]; +const parts: string[] = []; +for (let i = 0; i < 3000; i++) parts.push(`${words[i % words.length]}-${(i * 7919) % 1000}`); +const text = Buffer.from(parts.join(" "), "utf8"); + +let seed = 0x2545f491; +const binary = Buffer.alloc(20000); +for (let i = 0; i < binary.length; i++) { + seed = (seed * 1103515245 + 12345) >>> 0; + binary[i] = i % 3 === 0 ? seed >>> 24 : (i >> 4) & 0xff; +} + +const large = Buffer.alloc(1 << 20); +for (let i = 0; i < large.length; i++) large[i] = text[(i * 31) % text.length]! ^ ((i >> 12) & 7); + +for (const level of [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) { + const deflated = deflateSync(text, { level }); + console.log("deflate", level, digest(deflated), inflateSync(deflated).equals(text)); + console.log("raw", level, digest(deflateRawSync(binary, { level }))); + console.log("gzip", level, digest(gzipSync(text, { level }))); +} + +const variants: [string, ZlibOptions][] = [ + ["filtered", { strategy: constants.Z_FILTERED }], + ["huffman only", { strategy: constants.Z_HUFFMAN_ONLY }], + ["rle", { strategy: constants.Z_RLE }], + ["fixed", { strategy: constants.Z_FIXED }], + ["memLevel 1", { memLevel: 1 }], + ["memLevel 9", { memLevel: 9 }], + ["windowBits 9", { windowBits: 9 }], + ["windowBits 12", { windowBits: 12 }], + ["level 9 memLevel 9", { level: 9, memLevel: 9 }], + ["dictionary", { dictionary: Buffer.from("stream pipeline buffer deflate window") }], +]; +for (const [label, options] of variants) { + const deflated = deflateSync(text, options); + console.log(label, digest(deflated), digest(deflateRawSync(binary, options))); + console.log(label, "inflates", inflateSync(deflated, options).equals(text)); +} + +for (const level of [1, 6, 9]) { + const gz = gzipSync(large, { level }); + console.log("1 MiB gzip", level, digest(gz), gunzipSync(gz).equals(large)); + const raw = deflateRawSync(large, { level, windowBits: 10 }); + console.log("1 MiB raw windowBits 10", level, digest(raw), inflateRawSync(raw, { windowBits: 10 }).equals(large)); +} + +function collect(stream: Deflate | Gzip, label: string, done: () => void): void { + const chunks: Buffer[] = []; + stream.on("data", (chunk: Buffer) => chunks.push(chunk)); + stream.on("end", () => { + console.log(label, digest(Buffer.concat(chunks))); + done(); + }); +} + +const flushed = createDeflate({ level: 6 }); +collect(flushed, "stream sync flush", () => { + const changed = createGzip({ level: 1 }); + collect(changed, "stream params", () => console.log("done")); + changed.write(text.subarray(0, 8000)); + changed.flush(constants.Z_FULL_FLUSH, () => { + changed.params(9, constants.Z_DEFAULT_STRATEGY, () => { + changed.write(text.subarray(8000)); + changed.end(binary); + }); + }); +}); +flushed.write(text.subarray(0, 5000)); +flushed.flush(constants.Z_SYNC_FLUSH, () => { + flushed.write(binary); + flushed.end(text.subarray(5000)); +}); diff --git a/tests/corpus/8406-zlib-inflate-chunks.cjs b/tests/corpus/8406-zlib-inflate-chunks.cjs new file mode 100644 index 000000000..f8fade68f --- /dev/null +++ b/tests/corpus/8406-zlib-inflate-chunks.cjs @@ -0,0 +1,69 @@ +// Inflation over the vendored zlib's chunked inflate: inputs fed in pieces of +// 1 to 4096 bytes, output chunks of 64 bytes to 64 KiB, raw windowBits 9 to 15, +// concatenated gzip members, back-references across a 1 MiB window, and a +// truncated input. The output and the error match Node's. +const zlib = require("node:zlib"); +const { createHash } = require("node:crypto"); + +const digest = (bytes) => `${bytes.length} ${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`; + +const parts = []; +for (let i = 0; i < 20000; i++) parts.push(`row ${i % 977} ${"abcdefgh".slice(i % 8)} ${(i * 2654435761) % 4093}`); +const text = Buffer.from(parts.join("\n")); +const large = Buffer.alloc(1 << 20); +for (let i = 0; i < large.length; i++) large[i] = text[(i * 13) % text.length] ^ ((i >> 15) & 3); + +const gz = zlib.gzipSync(large, { level: 6 }); +for (const chunkSize of [64, 1024, 16384, 65536]) { + console.log("gunzip chunkSize", chunkSize, digest(zlib.gunzipSync(gz, { chunkSize }))); +} +for (const windowBits of [9, 10, 12, 15]) { + const raw = zlib.deflateRawSync(text, { windowBits, level: 9 }); + console.log("raw windowBits", windowBits, digest(zlib.inflateRawSync(raw, { windowBits }))); +} +const members = Buffer.concat([zlib.gzipSync(text.subarray(0, 1000)), zlib.gzipSync(text.subarray(1000, 5000))]); +console.log("members", digest(zlib.gunzipSync(members))); +try { + zlib.inflateSync(zlib.deflateSync(text).subarray(0, 2000)); +} catch (e) { + console.log("truncated", e.code, e.errno, e.message); +} + +function feed(input, pieceSize, outSize, next) { + const inflate = zlib.createGunzip({ chunkSize: outSize }); + const hash = createHash("sha256"); + let length = 0; + inflate.on("data", (chunk) => { + hash.update(chunk); + length += chunk.length; + }); + inflate.on("end", () => { + console.log("stream piece", pieceSize, "chunk", outSize, length, hash.digest("hex").slice(0, 16)); + next(); + }); + let offset = 0; + const pump = () => { + while (offset < input.length) { + const piece = input.subarray(offset, offset + pieceSize); + offset += pieceSize; + if (!inflate.write(piece)) { + inflate.once("drain", pump); + return; + } + } + inflate.end(); + }; + pump(); +} + +const head = zlib.gzipSync(large.subarray(0, 1 << 16), { level: 6 }); +const runs = [ + [head, 1, 64], + [gz, 7, 1024], + [gz, 333, 4096], + [gz, 4096, 65536], +]; +const step = (i) => { + if (i < runs.length) feed(runs[i][0], runs[i][1], runs[i][2], () => step(i + 1)); +}; +step(0); diff --git a/tests/corpus/8407-string-index-of-from-ascii.ts b/tests/corpus/8407-string-index-of-from-ascii.ts new file mode 100644 index 000000000..04582b7cd --- /dev/null +++ b/tests/corpus/8407-string-index-of-from-ascii.ts @@ -0,0 +1,38 @@ +// indexOf with a start index on ASCII receivers: integral, fractional, +// negative, -0, NaN, Infinity and out-of-range starts; empty needles, which +// are found at the clamped start; needles longer than the rest; needles with +// non-ASCII characters and lone surrogates, which an ASCII receiver never +// contains; and receivers built at run time (slices, concatenations, readline +// style splits) next to non-ASCII ones. The results match Node's. +const line = `10.1.2.3 - - [24/Oct/2026:10:00:00 +0000] "GET /api/items/7?page=3 HTTP/1.1" 200 5120 "agent"`; +const starts = [0, 1, 2.7, -1, -0, NaN, Infinity, -Infinity, line.length - 1, line.length, line.length + 5, 1e21]; +const needles = ["", '"', " ", "/", "api", "HTTP/1.1", line, line + "x", "é", "\ud83d", "\udca9", "💩"]; + +for (const needle of needles) { + const found = starts.map((start) => line.indexOf(needle, start)); + console.log(JSON.stringify(needle), found.join(" ")); +} + +const quote = line.indexOf('"'); +const space = line.indexOf(" ", quote); +const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1)); +const close = line.indexOf('"', space); +console.log(quote, space, path, close, line.slice(close + 2, close + 5)); + +const built = ["alpha", "beta", "gamma"].join("/") + "/" + String(42); +for (let i = 0; i <= built.length + 1; i++) { + console.log(i, built.indexOf("/", i), built.indexOf("a", i), built.indexOf("", i), built.indexOf("42", i)); +} + +const mixed = "café/naïve/ascii/end"; +const ascii = mixed.slice(mixed.indexOf("ascii")); +console.log(ascii, ascii.indexOf("/", 1), ascii.indexOf("end", 3), mixed.indexOf("/", 5), mixed.indexOf("ascii", 6)); + +let count = 0; +let at = 0; +const csv = Array.from({ length: 200 }, (_, i) => `${i},${i * 3},row${i}`).join(","); +while ((at = csv.indexOf(",", at)) !== -1) { + count++; + at += 1.5; +} +console.log("commas", count); diff --git a/tests/corpus/8410-dynamic-class-instance-reads.js b/tests/corpus/8410-dynamic-class-instance-reads.js new file mode 100644 index 000000000..83b9cad9b --- /dev/null +++ b/tests/corpus/8410-dynamic-class-instance-reads.js @@ -0,0 +1,62 @@ +// Untyped reads and writes of class instances at property sites: base and +// subclass instances, subclass instances returned where the base class is +// the inferred type, a subclass that redeclares an inherited field, a +// getter, fields set after construction and fields never set. +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +} +class Tagged extends Point { + constructor(x, y, tag) { + super(x, y); + this.tag = tag; + } +} +class Shifted extends Point { + label = "s"; + x = 100; +} +class Gauge { + get x() { + return 9; + } +} +class Late { + constructor(set) { + if (set) this.x = "late"; + } +} +function make(i) { + if (i % 3 === 0) return new Point(i, -i); + if (i % 3 === 1) return new Tagged(i, i * 2, "t" + i); + return new Shifted(i, 0); +} +function readX(o) { + return o.x; +} +function readY(o) { + return o.y; +} +function writeX(o, v) { + o.x = v; +} +const values = []; +for (let i = 0; i < 9; i++) values.push(make(i)); +values.push(new Gauge(), new Late(true), new Late(false), { x: "plain", y: 1 }, { y: 2, x: "swapped" }); +let sum = 0; +for (let round = 0; round < 3; round++) { + for (const v of values) { + const x = readX(v); + sum += typeof x === "number" ? x : x === undefined ? 1000 : x.length; + const y = readY(v); + if (typeof y === "number") sum += y; + } + for (let i = 0; i < 9; i++) writeX(values[i], round * 10 + i); +} +console.log(sum); +console.log(values.map((v) => String(readX(v))).join(" ")); +console.log(values.map((v) => String(readY(v))).join(" ")); +writeX(values[1], "changed"); +console.log(readX(values[1]), readX(values[2]), values[2].label, values[1].tag); diff --git a/tests/corpus/8411-dynamic-base-keyed-instances/main.ts b/tests/corpus/8411-dynamic-base-keyed-instances/main.ts new file mode 100644 index 000000000..5c0caa936 --- /dev/null +++ b/tests/corpus/8411-dynamic-base-keyed-instances/main.ts @@ -0,0 +1,32 @@ +// Untyped property reads of class instances that typed code passes as their +// base class: subclass instances in a Point[] read their inherited field and +// their own field through an untyped module. +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const reader: { + readX(o: unknown): unknown; + readTag(o: unknown): unknown; + sum(list: unknown[]): number; +} = require("./reader.cjs"); + +class Point { + x: any; + constructor(x: any) { + this.x = x; + } +} +class Tagged extends Point { + tag: any; + constructor(x: any, tag: any) { + super(x); + this.tag = tag; + } +} +function make(i: number): Point { + return i % 2 === 0 ? new Point(i) : new Tagged(i * 10, "t" + i); +} +const list: Point[] = []; +for (let i = 0; i < 6; i++) list.push(make(i)); +console.log(list.map((p) => String(reader.readX(p))).join(" ")); +console.log(list.map((p) => String(reader.readTag(p))).join(" ")); +console.log(reader.sum(list)); diff --git a/tests/corpus/8411-dynamic-base-keyed-instances/package.json b/tests/corpus/8411-dynamic-base-keyed-instances/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/8411-dynamic-base-keyed-instances/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/8411-dynamic-base-keyed-instances/reader.cjs b/tests/corpus/8411-dynamic-base-keyed-instances/reader.cjs new file mode 100644 index 000000000..941299ed5 --- /dev/null +++ b/tests/corpus/8411-dynamic-base-keyed-instances/reader.cjs @@ -0,0 +1,11 @@ +exports.readX = function (o) { + return o.x; +}; +exports.readTag = function (o) { + return o.tag; +}; +exports.sum = function (list) { + let s = 0; + for (let i = 0; i < list.length; i++) s += list[i].x; + return s; +}; diff --git a/tests/corpus/arena-array-subclass.ts b/tests/corpus/arena-array-subclass.ts new file mode 100644 index 000000000..281d07823 --- /dev/null +++ b/tests/corpus/arena-array-subclass.ts @@ -0,0 +1,63 @@ +// Builders whose groups hold instances of a class that extends Array, +// beside a builder whose group takes an arena. An Array subclass's +// instance is an array, so a group that allocates one keeps its objects on +// the heap; the results and their teardown match Node either way. +class Row extends Array { + sum(): number { + let s = 0; + for (const v of this) s += v; + return s; + } +} + +class Branch { + row: Row; + left: Branch | null; + right: Branch | null; + constructor(row: Row, left: Branch | null, right: Branch | null) { + this.row = row; + this.left = left; + this.right = right; + } + total(): number { + return this.row.sum() + (this.left ? this.left.total() : 0) + (this.right ? this.right.total() : 0); + } +} + +class Pair { + left: Pair | null; + right: Pair | null; + constructor(left: Pair | null, right: Pair | null) { + this.left = left; + this.right = right; + } + count(): number { + return 1 + (this.left ? this.left.count() : 0) + (this.right ? this.right.count() : 0); + } +} + +function row(n: number): Row { + const r = new Row(); + for (let j = 0; j <= n; j++) r.push(j); + return r; +} + +function branches(depth: number): Branch { + return depth === 0 + ? new Branch(row(depth), null, null) + : new Branch(row(depth), branches(depth - 1), branches(depth - 1)); +} + +function pairs(depth: number): Pair { + return depth === 0 ? new Pair(null, null) : new Pair(pairs(depth - 1), pairs(depth - 1)); +} + +let checks = 0; +for (let round = 0; round < 20; round++) { + const b = branches(6); + const p = pairs(8); + const r = row(round); + checks += b.total() + p.count() + r.sum() + r.length; + if (round === 0) console.log(b.total(), b.row.length, b.row instanceof Row, p.count(), r.sum()); +} +console.log(checks); diff --git a/tests/corpus/arena-arrays.ts b/tests/corpus/arena-arrays.ts new file mode 100644 index 000000000..fb2134231 --- /dev/null +++ b/tests/corpus/arena-arrays.ts @@ -0,0 +1,49 @@ +// Groups with arrays: child lists that grow past their first storage, +// back references to parents and siblings, and lists of lists. +class Node { + parent: Node | undefined = undefined; + children: Node[] = []; + sibling: Node | undefined = undefined; + weight: number; + constructor(weight: number) { + this.weight = weight; + } +} + +function wide(depth: number, fan: number, parent: Node | undefined, weight: number): Node { + const node = new Node(weight); + node.parent = parent; + if (depth > 0) { + for (let i = 0; i < fan; i++) node.children.push(wide(depth - 1, fan, node, weight * 3 + i)); + for (let i = 0; i < fan; i++) node.children[i]!.sibling = node.children[(i + 1) % fan]!; + } + return node; +} + +function weigh(node: Node): number { + let total = node.weight; + for (const child of node.children) total += weigh(child); + return total % 1000003; +} + +let w = 0; +for (let round = 0; round < 30; round++) w = (w + weigh(wide(2 + (round % 3), 3 + (round % 20), undefined, round))) % 1000003; +console.log("weights", w); + +function grid(rows: number, cols: number): number[][] { + const out: number[][] = []; + for (let r = 0; r < rows; r++) { + const row: number[] = []; + for (let c = 0; c < cols; c++) row.push(r * cols + c); + out.push(row); + } + return out; +} +let cells = 0; +for (let k = 1; k < 40; k++) { + const g = grid(k, 70); + cells += g.length + g[k - 1]![69]! + g[0]!.length; +} +console.log("cells", cells); +const t = wide(2, 4, undefined, 1); +console.log(t.children.length, t.children[1]!.parent === t, t.children[3]!.sibling === t.children[0], t.children[2]!.children[3]!.weight); diff --git a/tests/corpus/arena-cap.ts b/tests/corpus/arena-cap.ts new file mode 100644 index 000000000..1ab1f43aa --- /dev/null +++ b/tests/corpus/arena-cap.ts @@ -0,0 +1,30 @@ +// A group too large for one arena: past its byte cap the rest of the +// group goes to the heap, and both parts are released together. +interface Cell { + index: number; + label: string; + next: Cell | null; +} + +function list(n: number): Cell { + let head: Cell = { index: 0, label: "c0", next: null }; + const first = head; + for (let i = 1; i < n; i++) { + const cell: Cell = { index: i, label: i % 1000 === 0 ? `c${i}` : "c", next: null }; + head.next = cell; + head = cell; + } + return first; +} + +function walk(c: Cell | null): number { + let total = 0; + let labels = 0; + for (let at = c; at !== null; at = at.next) { + total += at.index; + if (at.label.length > 1) labels++; + } + return total + labels; +} + +for (let round = 0; round < 3; round++) console.log("cells", walk(list(900000 + round))); diff --git a/tests/corpus/arena-escapes.ts b/tests/corpus/arena-escapes.ts new file mode 100644 index 000000000..f65a34e87 --- /dev/null +++ b/tests/corpus/arena-escapes.ts @@ -0,0 +1,72 @@ +// Values built in a group that leave it: handed to callbacks that keep +// them, stored by a method into a module list, kept from a getter, pushed +// into a caller's list, or captured by a returned closure. None of those +// groups takes an arena; the one whose values stay in the frame does. +class Node { + kids: Node[] = []; + label: string; + constructor(label: string) { + this.label = label; + } + get first(): Node { + return this.kids[0]!; + } + register(): void { + registry.push(this); + } +} +const registry: Node[] = []; +const saved: Node[] = []; +const held = new Map(); +function build(depth: number, label: string): Node { + const n = new Node(label); + for (let i = 0; depth > 0 && i < 3; i++) n.kids.push(build(depth - 1, `${label}.${i}`)); + return n; +} +function each(depth: number, cb: (n: Node) => void): number { + const t = build(depth, "e"); + cb(t); + return t.kids.length; +} +function visit(n: Node, cb: (x: Node) => void): void { + cb(n); + for (const k of n.kids) visit(k, cb); +} +function forward(depth: number, cb: (x: Node) => void): number { + const t = build(depth, "f"); + visit(t, cb); + return t.kids.length; +} +function buildInto(sink: Node[], depth: number): number { + const n = build(depth, "s"); + sink.push(n.kids[0]!); + return n.kids.length; +} +function closureOf(depth: number): () => string { + const n = build(depth, "c"); + return () => n.kids[1]!.label; +} +let total = 0; +for (let r = 0; r < 8; r++) { + total += each(2, (n) => saved.push(n.kids[1]!)); + total += forward(2, (x) => { + if (x.label.endsWith(".2")) saved.push(x); + }); + const t = build(2, `t${r}`); + t.kids.forEach((k) => { + if (k.label.endsWith(".0")) saved.push(k); + }); + const u = build(2, `u${r}`); + for (const k of u.kids) if (k.label.endsWith(".1")) saved.push(k); + const v = build(2, `v${r}`); + held.set(r, v.first); + const a = build(2, `a${r}`); + if (r % 3 === 0) a.kids[0]!.register(); + total += buildInto(saved, 2); + const w = build(2, `w${r}`); + total += w.kids.length + w.first.kids.length; +} +console.log(total, saved.length, saved.map((n) => n.label + "/" + n.kids.length).slice(0, 10).join(",")); +const f = closureOf(2); +console.log(f(), registry.map((n) => n.label + n.kids.length).join(",")); +console.log([...held.values()].map((n) => n.label + n.kids[0]!.label).join(",")); diff --git a/tests/corpus/arena-mixed.ts b/tests/corpus/arena-mixed.ts new file mode 100644 index 000000000..ff6c8e76c --- /dev/null +++ b/tests/corpus/arena-mixed.ts @@ -0,0 +1,80 @@ +// Groups built through virtual calls and sorted with callbacks, nested +// groups inside a group, and temporaries the group does not keep. +abstract class Shape { + abstract area(): number; + abstract scaled(k: number): Shape; +} +class Square extends Shape { + side: number; + constructor(side: number) { + super(); + this.side = side; + } + area(): number { + return this.side * this.side; + } + scaled(k: number): Shape { + return new Square(this.side * k); + } +} +class Circle extends Shape { + radius: number; + constructor(radius: number) { + super(); + this.radius = radius; + } + area(): number { + return Math.round(Math.PI * this.radius * this.radius * 100) / 100; + } + scaled(k: number): Shape { + return new Circle(this.radius * k); + } +} + +interface Box { + label: string; + shapes: Shape[]; +} + +function measure(s: Shape): { area: number } { + return { area: s.area() }; +} + +function boxes(n: number): Box[] { + const out: Box[] = []; + for (let i = 0; i < n; i++) { + const shapes: Shape[] = []; + for (let j = 0; j < 4; j++) { + const base: Shape = j % 2 === 0 ? new Square(j + i) : new Circle(j + 1); + shapes.push(base.scaled(2)); + } + // Temporaries the group does not keep. + let total = 0; + for (const s of shapes) total += measure(s).area; + shapes.sort((a, b) => a.area() - b.area()); + out.push({ label: `box${i}:${Math.round(total)}`, shapes }); + } + return out; +} + +function summarize(n: number): string { + const list = boxes(n); + let largest = ""; + let best = -1; + for (const b of list) { + const top = b.shapes[b.shapes.length - 1]!.area(); + if (top > best) { + best = top; + largest = b.label; + } + } + return `${largest}@${best}`; +} + +let out = ""; +for (let round = 1; round < 30; round++) { + const all = boxes(round); + out = `${all.length} ${all[0]!.label} ${all[round - 1]!.shapes[0]!.area()} ${summarize(round)}`; + if (round % 7 === 0) console.log(out); +} +console.log(out); diff --git a/tests/corpus/arena-outside.ts b/tests/corpus/arena-outside.ts new file mode 100644 index 000000000..cc965d5d3 --- /dev/null +++ b/tests/corpus/arena-outside.ts @@ -0,0 +1,49 @@ +// Groups whose members refer to objects outside them: a long-lived +// registry, strings built along the way, and a parameter's object. The +// arena's drop releases those references; the outside objects stay usable. +class Category { + members = 0; + name: string; + constructor(name: string) { + this.name = name; + } +} + +class Member { + next: Member | undefined = undefined; + category: Category; + label: string; + rank: number; + constructor(category: Category, label: string, rank: number) { + this.category = category; + this.label = label; + this.rank = rank; + category.members++; + } +} + +function ring(categories: Category[], size: number, seed: number): Member { + const first = new Member(categories[seed % categories.length]!, `m${seed}`, seed); + let last = first; + for (let i = 1; i < size; i++) { + const member = new Member(categories[(seed + i * 7) % categories.length]!, `m${seed}-${i}`, seed + i); + last.next = member; + last = member; + } + last.next = first; + return first; +} + +const categories: Category[] = []; +for (let i = 0; i < 5; i++) categories.push(new Category(`cat-${i}`)); + +let ranks = 0; +let labels = ""; +for (let round = 0; round < 200; round++) { + const r = ring(categories, 30, round); + ranks += r.next!.rank + r.next!.next!.rank; + if (round % 50 === 0) labels += `${r.label}:${r.category.name}:${r.next!.label} `; +} +console.log("ranks", ranks); +console.log("labels", labels.trim()); +for (const c of categories) console.log(c.name, c.members); diff --git a/tests/corpus/arena-region-throws.ts b/tests/corpus/arena-region-throws.ts new file mode 100644 index 000000000..073dca966 --- /dev/null +++ b/tests/corpus/arena-region-throws.ts @@ -0,0 +1,33 @@ +// Exceptions inside call regions: the region's arena is dropped as the +// exception leaves the call, and the caller's own objects are untouched. +interface Entry { + key: string; + value: number; + next: Entry | null; +} + +function tally(keys: string[], limit: number): number { + const entries: Entry[] = []; + for (const k of keys) { + entries.push({ key: k, value: k.length, next: entries.length > 0 ? entries[entries.length - 1]! : null }); + if (entries.length > limit) throw new RangeError(`too many entries: ${entries.length}`); + } + let sum = 0; + for (const e of entries) sum += e.value + (e.next === null ? 0 : e.next.value); + return sum; +} + +const kept: Entry[] = [{ key: "outside", value: 1, next: null }]; +const keys = "alpha beta gamma delta epsilon zeta eta theta iota kappa".split(" "); +let total = 0; +let errors = 0; +for (let round = 0; round < 40; round++) { + try { + total += tally(keys, 3 + (round % 10)); + } catch (e) { + errors++; + if (round < 6) console.log((e as Error).name, (e as Error).message); + } + kept.push({ key: `k${round}`, value: round, next: kept[kept.length - 1]! }); +} +console.log("total", total, "errors", errors, "kept", kept.length, kept[kept.length - 1]!.next!.key); diff --git a/tests/corpus/arena-regions.ts b/tests/corpus/arena-regions.ts new file mode 100644 index 000000000..cd1aa9679 --- /dev/null +++ b/tests/corpus/arena-regions.ts @@ -0,0 +1,44 @@ +// Call regions: calls of functions that build what they need and return +// a number, so everything they allocate is dead when they return. +interface Point { + x: number; + y: number; +} +interface Segment { + from: Point; + to: Point; +} + +function perimeter(n: number, scale: number): number { + const points: Point[] = []; + for (let i = 0; i < n; i++) points.push({ x: Math.cos(i) * scale, y: Math.sin(i) * scale }); + const segments: Segment[] = []; + for (let i = 0; i < n; i++) segments.push({ from: points[i]!, to: points[(i + 1) % n]! }); + let total = 0; + for (const s of segments) total += Math.hypot(s.to.x - s.from.x, s.to.y - s.from.y); + return Math.round(total * 1000) / 1000; +} + +let sum = 0; +for (let k = 3; k < 300; k++) sum += perimeter(k, k / 10); +console.log("perimeters", sum.toFixed(3)); + +function histogram(words: string[]): number { + const buckets: { word: string; count: number }[] = []; + for (const w of words) { + let found = false; + for (const b of buckets) + if (b.word === w) { + b.count++; + found = true; + } + if (!found) buckets.push({ word: w, count: 1 }); + } + let best = 0; + for (const b of buckets) best = Math.max(best, b.count * 100 + b.word.length); + return best; +} +const words = "the quick brown fox jumps over the lazy dog the end".split(" "); +let h = 0; +for (let i = 0; i < 200; i++) h += histogram(words); +console.log("histogram", h); diff --git a/tests/corpus/arena-thrown-member.ts b/tests/corpus/arena-thrown-member.ts new file mode 100644 index 000000000..68382075c --- /dev/null +++ b/tests/corpus/arena-thrown-member.ts @@ -0,0 +1,36 @@ +// A thrown value that holds an object built in a group: the group cannot +// take an arena, because the value outlives it in the catch handler. +class Node { + label: string; + weight: number; + constructor(label: string, weight: number) { + this.label = label; + this.weight = weight; + } +} +class BuildError { + node: Node; + constructor(node: Node) { + this.node = node; + } +} +function build(n: number, failAt: number): Node[] { + const out: Node[] = []; + for (let i = 0; i < n; i++) { + const node = new Node(`n${i}`, i); + out.push(node); + if (i === failAt) throw new BuildError(node); + } + return out; +} +let total = 0; +const saved: string[] = []; +for (let r = 0; r < 20; r++) { + try { + total += build(50, r % 4 === 0 ? 30 : -1).length; + } catch (e) { + const b = e as BuildError; + saved.push(b.node.label + ":" + b.node.weight); + } +} +console.log(total, saved.join(",")); diff --git a/tests/corpus/arena-throws.ts b/tests/corpus/arena-throws.ts new file mode 100644 index 000000000..b3dbd3c16 --- /dev/null +++ b/tests/corpus/arena-throws.ts @@ -0,0 +1,40 @@ +// A builder that throws part way: the arena of the group it was building +// is dropped as the exception leaves, and building goes on afterwards. +interface Item { + id: number; + tags: string[]; + next: Item | null; +} + +function chain(n: number, failAt: number): Item { + let head: Item = { id: 0, tags: ["head"], next: null }; + const first = head; + for (let i = 1; i < n; i++) { + if (i === failAt) throw new Error(`failed at ${i} of ${n}`); + const item: Item = { id: i, tags: [`t${i}`, `u${i % 3}`], next: null }; + head.next = item; + head = item; + } + return first; +} + +function length(item: Item | null): number { + let n = 0; + for (let at = item; at !== null; at = at.next) n++; + return n; +} + +let built = 0; +let failed = 0; +for (let round = 0; round < 60; round++) { + try { + const c = chain(20 + (round % 7), round % 5 === 0 ? 10 + (round % 3) : -1); + built += length(c) + c.next!.tags.length; + } catch (e) { + failed++; + if (round < 20) console.log("caught", (e as Error).message); + } +} +console.log("built", built, "failed", failed); +const last = chain(5, -1); +console.log(last.id, last.next!.id, last.next!.tags.join("/"), length(last)); diff --git a/tests/corpus/arena-trees.ts b/tests/corpus/arena-trees.ts new file mode 100644 index 000000000..eb736dbab --- /dev/null +++ b/tests/corpus/arena-trees.ts @@ -0,0 +1,70 @@ +// Recursive builders whose results their callers only read: each tree is a +// group that lives in an arena until the statement (or the loop body that +// holds it) ends. +class TreeNode { + left: TreeNode | null; + right: TreeNode | null; + value: number; + constructor(left: TreeNode | null, right: TreeNode | null, value: number) { + this.left = left; + this.right = right; + this.value = value; + } + sum(): number { + return this.value + (this.left === null ? 0 : this.left.sum()) + (this.right === null ? 0 : this.right.sum()); + } + depth(): number { + return 1 + Math.max(this.left === null ? 0 : this.left.depth(), this.right === null ? 0 : this.right.depth()); + } +} + +function build(depth: number, value: number): TreeNode { + return depth === 0 + ? new TreeNode(null, null, value) + : new TreeNode(build(depth - 1, value * 2), build(depth - 1, value * 2 + 1), value); +} + +let total = 0; +for (let d = 0; d < 12; d++) total += build(d, 1).sum(); +console.log("sums", total); + +for (let d = 2; d <= 4; d++) { + const t = build(d, 1); + console.log("tree", d, t.sum(), t.depth(), t.left!.value, t.right!.right!.value); +} + +function deepest(n: number): number { + let best = 0; + for (let i = 0; i < n; i++) { + const t = build(i % 6, i); + best = Math.max(best, t.depth() * 1000 + (t.sum() % 1000)); + } + return best; +} +console.log("deepest", deepest(40)); + +// Record-literal trees: a root call whose result its caller only reads +// builds the whole tree, its root node included, in the group's arena. +interface Node2 { + left: Node2 | null; + right: Node2 | null; + value: number; +} + +function buildRecord(depth: number, value: number): Node2 { + return depth === 0 + ? { left: null, right: null, value } + : { left: buildRecord(depth - 1, value * 2), right: buildRecord(depth - 1, value * 2 + 1), value }; +} + +function sumRecord(node: Node2): number { + return node.value + (node.left === null ? 0 : sumRecord(node.left)) + (node.right === null ? 0 : sumRecord(node.right)); +} + +let recordTotal = 0; +for (let d = 0; d < 12; d++) recordTotal += sumRecord(buildRecord(d, 1)); +console.log("record sums", recordTotal); +for (let d = 1; d <= 3; d++) { + const t = buildRecord(d, 1); + console.log("record tree", d, sumRecord(t), t.value, t.left!.value, t.right!.value); +} diff --git a/tests/corpus/arena-workers.ts b/tests/corpus/arena-workers.ts new file mode 100644 index 000000000..b08a49748 --- /dev/null +++ b/tests/corpus/arena-workers.ts @@ -0,0 +1,75 @@ +// Arena groups in worker threads: each worker builds and reads trees of +// classes and record literals, groups that live in the worker's own arena +// until the statement or loop body that holds them ends. The main thread +// builds its own groups meanwhile and keeps one tree for the whole run. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; + +class Pair { + left: Pair | null; + right: Pair | null; + label: string; + constructor(left: Pair | null, right: Pair | null, label: string) { + this.left = left; + this.right = right; + this.label = label; + } +} + +interface Leaf { + left: Leaf | null; + right: Leaf | null; + weight: number; +} + +function pairs(depth: number, label: string): Pair { + return depth === 0 ? new Pair(null, null, label) : new Pair(pairs(depth - 1, label + "l"), pairs(depth - 1, label + "r"), label); +} + +function leaves(depth: number, weight: number): Leaf { + return depth === 0 + ? { left: null, right: null, weight } + : { left: leaves(depth - 1, weight + 1), right: leaves(depth - 1, weight * 2), weight }; +} + +function countPairs(p: Pair): number { + return 1 + (p.left === null ? 0 : countPairs(p.left)) + (p.right === null ? 0 : countPairs(p.right)); +} + +function rightmost(p: Pair): string { + return p.right === null ? p.label : rightmost(p.right); +} + +function weigh(l: Leaf): number { + return l.weight + (l.left === null ? 0 : weigh(l.left)) + (l.right === null ? 0 : weigh(l.right)); +} + +function task(depth: number, rounds: number): string { + let nodes = 0; + let total = 0; + for (let i = 0; i < rounds; i++) { + nodes += countPairs(pairs(depth, "p")); + total += weigh(leaves(depth, i)); + } + const kept = pairs(depth, "k"); + return `depth ${depth}: ${nodes} pairs, weight ${total}, rightmost ${rightmost(kept)}, kept ${countPairs(kept)}`; +} + +if (isMainThread) { + const longLived = leaves(10, 1); + const depths = [3, 5, 7, 9]; + const lines: string[] = new Array(depths.length).fill(""); + let pending = depths.length; + depths.forEach((depth, index) => { + const worker = new Worker(new URL(import.meta.url), { workerData: depth }); + worker.on("message", (line: string) => { + lines[index] = line; + if (--pending === 0) { + for (const l of lines) console.log(l); + console.log("main", task(6, 20)); + console.log("long lived", weigh(longLived)); + } + }); + }); +} else { + parentPort!.postMessage(task(workerData as number, 50)); +} 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/array-subclass-element-cycles.ts b/tests/corpus/array-subclass-element-cycles.ts new file mode 100644 index 000000000..375fe6faf --- /dev/null +++ b/tests/corpus/array-subclass-element-cycles.ts @@ -0,0 +1,60 @@ +// A class that extends Array whose elements can lead back to it only +// through a field of plain array type (TreeNode.kids holds a Kids, typed as +// an array of TreeNode): its instances carry the collector header that +// arrays of its element type carry, so the collector can visit them through +// that field. Forest's elements are Kids instances, which carry one too. +class Kids extends Array { + label = "kids"; + total(): number { + let sum = 0; + for (const kid of this) sum += kid.size(); + return sum; + } +} + +class TreeNode { + kids = new Kids(); + value: number; + constructor(value: number) { + this.value = value; + } + size(): number { + return 1 + this.kids.total(); + } +} + +class Forest extends Array { + name = "forest"; +} + +function grow(depth: number): TreeNode { + const node = new TreeNode(depth); + if (depth > 0) for (let i = 0; i < 3; i++) node.kids.push(grow(depth - 1)); + return node; +} + +// Garbage cycles through the field of array type make the collector run +// while the trees are alive. +function churn(rounds: number): number { + let made = 0; + for (let i = 0; i < rounds; i++) { + const node = new TreeNode(i); + node.kids.push(node); + made++; + } + return made; +} + +let checks = 0; +for (let round = 0; round < 20; round++) { + checks += grow(4).size(); + churn(500); +} +console.log(checks); + +const forest = new Forest(); +for (let i = 0; i < 4; i++) forest.push(grow(2).kids); +churn(5000); +let leaves = 0; +for (const kids of forest) leaves += kids.total(); +console.log(forest.name, forest.length, leaves, forest[0]!.label, forest[0] instanceof Kids); diff --git a/tests/corpus/array-subclass-element-instanceof.ts b/tests/corpus/array-subclass-element-instanceof.ts new file mode 100644 index 000000000..f520e55a1 --- /dev/null +++ b/tests/corpus/array-subclass-element-instanceof.ts @@ -0,0 +1,37 @@ +// Instances of classes that extend Array read back from an array of them +// (an element read is `Row | undefined`) still answer instanceof for their +// own class and Array, false for another Array subclass, and plain arrays +// answer false. +class Row extends Array { + sum(): number { + let s = 0; + for (const v of this) s += v; + return s; + } +} +class Column extends Array {} + +function rows(n: number): Row[] { + const out: Row[] = []; + for (let i = 0; i < n; i++) { + const row = new Row(); + for (let j = 0; j <= i; j++) row.push(j); + out.push(row); + } + return out; +} + +const lines: number[][] = [new Row(), new Column(), [1, 2]]; + +const table = rows(6); +console.log(table.length, table[5]!.sum(), table[5] instanceof Row, table[5] instanceof Column); +console.log(table[4] instanceof Row, table[4] instanceof Array); +console.log(table.at(-1) instanceof Row, table.at(10) instanceof Row); +const plain: number[][] = [[1, 2]]; +console.log(plain[0] instanceof Row, plain[0] instanceof Array); +const kinds = lines.map((r) => (r instanceof Column ? "column" : r instanceof Row ? "row" : "plain")); +console.log(kinds.join(",")); +let checks = 0; +for (let round = 0; round < 20; round++) + checks += rows(6).reduce((s, r) => s + (r instanceof Row ? r.sum() : 0), 0); +console.log(checks); 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/discarded-logical-void-conditionals.mjs b/tests/corpus/discarded-logical-void-conditionals.mjs new file mode 100644 index 000000000..e804a28e3 --- /dev/null +++ b/tests/corpus/discarded-logical-void-conditionals.mjs @@ -0,0 +1,22 @@ +// React's development warnings discard a logical whose right operand is a +// conditional of console.error calls. +function warnIfMissingRender(instance, Component) { + instance.render || + (Component.prototype && typeof Component.prototype.render === "function" + ? console.error("%s(...): No `render` method found on the returned component.", "App") + : console.error("%s(...): No `render` method found, and its prototype has none.", "App")); +} +warnIfMissingRender({ render: null }, { prototype: { render() {} } }); +warnIfMissingRender({ render: null }, { prototype: {} }); +warnIfMissingRender({ render() {} }, { prototype: {} }); + +let warned = 0; +function warn(message) { + warned++; + console.log("warn:", message); +} +for (const props of [{ key: 1 }, {}, { key: "a", ref: null }]) { + "key" in props && (typeof props.key === "string" ? warn("string key") : warn("number key")); + props.ref === undefined || (props.ref === null ? warn("null ref") : warn("ref")); +} +console.log("warned", warned); diff --git a/tests/corpus/discarded-logical-void-conditionals.ts b/tests/corpus/discarded-logical-void-conditionals.ts new file mode 100644 index 000000000..1437957da --- /dev/null +++ b/tests/corpus/discarded-logical-void-conditionals.ts @@ -0,0 +1,69 @@ +// A logical expression whose result is discarded and whose right operand is +// a conditional of void calls (`skip || (flag ? warnA() : warnB())`) runs as +// a branch: the left operand decides whether the conditional runs, and the +// conditional runs one arm. +const order: string[] = []; +function check(label: string, value: boolean): boolean { + order.push(label); + return value; +} +function emit(label: string): void { + order.push(label); +} + +for (const left of [false, true]) { + for (const flag of [false, true]) { + check("and", left) && (check("flag", flag) ? emit("and:yes") : emit("and:no")); + check("or", left) || (check("flag", flag) ? emit("or:yes") : emit("or:no")); + left || (flag && (check("inner", !flag) ? emit("nested:yes") : emit("nested:no"))); + left || (order.push("comma"), flag ? emit("comma:yes") : emit("comma:no")); + !left && (flag ? (left ? emit("deep:a") : emit("deep:b")) : emit("deep:c")); + } +} +console.log(order.join(" ")); +order.length = 0; + +// A for loop's initializer and update discard their results too. +let steps = 0; +for ( + check("init", false) || (check("init flag", true) ? emit("init:yes") : emit("init:no")); + steps < 2; + check("update", true) && (steps > 1 ? emit("update:last") : emit("update:next")) +) { + steps++; +} +console.log(steps, order.join(" ")); +order.length = 0; + +// The left operand narrows the conditional. +function describe(value: string | number): void { + typeof value === "string" && (value.length > 2 ? emit(value.toUpperCase()) : emit(value)); + typeof value === "number" || (value === "" ? emit("empty") : emit(`text:${value}`)); +} +describe("abc"); +describe("ab"); +describe(""); +describe(7); +console.log(order.join(" ")); +order.length = 0; + +// A generic callback: the conditional's type is T, void for the first two +// calls and number for the last. +function choose(skip: boolean, flag: boolean, a: () => T, b: () => T): void { + skip || (flag ? a() : b()); +} +choose(false, true, () => emit("generic:a"), () => emit("generic:b")); +choose(false, false, () => emit("generic:a"), () => emit("generic:b")); +choose(true, true, () => emit("generic:skipped"), () => emit("generic:skipped")); +choose(false, true, () => order.push("number:a"), () => order.push("number:b")); +console.log(order.join(" ")); +order.length = 0; + +// A throw from the chosen arm propagates. +try { + choose(false, false, () => emit("unused"), () => { + throw new Error("from b"); + }); +} catch (error) { + console.log((error as Error).message); +} 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/frozen-record-untyped-writes.ts b/tests/corpus/frozen-record-untyped-writes.ts new file mode 100644 index 000000000..02ed6b5c6 --- /dev/null +++ b/tests/corpus/frozen-record-untyped-writes.ts @@ -0,0 +1,35 @@ +// A frozen native record or array seen through an untyped reference keeps +// its frozen state: strict-mode writes throw Node's TypeErrors and leave +// the value unchanged, and Object.isFrozen answers true through any name. +export {}; + +function write(target: unknown, key: string, value: unknown): void { + try { + (target as Record)[key] = value; + console.log("wrote", key); + } catch (e) { + console.log("threw", (e as Error).message); + } +} + +const literal = Object.freeze({ a: 1, b: "x" }); +function make(n: number) { + return Object.freeze({ a: n, tags: ["t"] }); +} +const made = make(2); +write(literal, "a", 5); +write(literal, "z", 5); +write(made, "a", 7); +write(made, "tags", []); +made.tags.push("u"); +console.log(literal.a, made.a, JSON.stringify(literal), JSON.stringify(made)); +console.log(Object.isFrozen(literal), Object.isFrozen(made), Object.isFrozen(made.tags)); + +const list = Object.freeze([1, 2]); +write(list, "0", 9); +write(list, "5", 9); +console.log(list[0], list.length, Object.isFrozen(list)); + +const open = { a: 1 }; +write(open, "a", 3); +console.log(open.a); diff --git a/tests/corpus/frozen-untyped-writes-sloppy.cjs b/tests/corpus/frozen-untyped-writes-sloppy.cjs new file mode 100644 index 000000000..c80f185a7 --- /dev/null +++ b/tests/corpus/frozen-untyped-writes-sloppy.cjs @@ -0,0 +1,43 @@ +// Sloppy-mode code ignores writes that strict mode refuses: assignments to +// frozen objects and arrays and to non-extensible objects leave them +// unchanged without an error, while strict functions still throw. +function write(target, key, value) { + target[key] = value; + console.log("after", key, JSON.stringify(target)); +} +function strictWrite(target, key, value) { + "use strict"; + try { + target[key] = value; + console.log("wrote", key); + } catch (e) { + console.log("threw", e.message); + } +} +const frozen = Object.freeze({ a: 1, b: "x" }); +write(frozen, "a", 5); +write(frozen, "z", 5); +strictWrite(frozen, "a", 5); +const closed = Object.preventExtensions(JSON.parse('{"a":1}')); +write(closed, "a", 2); +write(closed, "b", 2); +strictWrite(closed, "b", 2); +const list = Object.freeze([1, 2]); +write(list, "0", 9); +strictWrite(list, "0", 9); +const holder = {}; +Object.defineProperty(holder, "fixed", { value: 1, writable: false, enumerable: true }); +write(holder, "fixed", 2); +strictWrite(holder, "fixed", 2); +const withSetter = { + /** @param {number} x */ + set v(x) { + throw new Error("setter " + x); + }, +}; +try { + write(withSetter, "v", 1); +} catch (e) { + console.log("setter threw", e.message); +} +console.log(frozen.a, closed.b, list[0], holder.fixed); 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/integer-clone-int32-slots.ts b/tests/corpus/integer-clone-int32-slots.ts new file mode 100644 index 000000000..5c34ada67 --- /dev/null +++ b/tests/corpus/integer-clone-int32-slots.ts @@ -0,0 +1,40 @@ +// A recursive function over a number array runs as an integer clone. +// sumFrom only receives arrays that hold int32s, which keep 4-byte int32 +// slots, and the clone reads them at that width, near their end too. +// sumWide receives an array that also holds a fraction, with 8-byte slots. +function sumFrom(xs: number[], i: number): number { + if (i >= xs.length) return 0; + return xs[i]! + sumFrom(xs, i + 1); +} + +function masked(n: number, start: number): number { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(i & 7); + return sumFrom(a, start); +} + +function halves(n: number, start: number): number { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(i < n / 2 ? 0 : 3); + return sumFrom(a, start); +} + +function sumWide(xs: number[], i: number): number { + if (i >= xs.length) return 0; + return xs[i]! + sumWide(xs, i + 1); +} + +function widened(n: number, start: number): number { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(i % 5); + const before = sumWide(a, start); + a.push(0.5); + return before * 10 + sumWide(a, start); +} + +const { n } = JSON.parse('{"n": 1000}') as { n: number }; +for (const start of [0, 1, n / 2, n - 9, n - 2, n - 1, n]) + console.log(start, masked(n | 0, start | 0), halves(n | 0, start | 0)); +for (const size of [63, 64, 65]) + console.log(size, halves(size, size - 24), halves(size, size - 1), masked(size, size - 3)); +console.log(widened(40, 0), widened(40, 37)); 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-degraded-initializer.ts b/tests/corpus/localized-failure-degraded-initializer.ts new file mode 100644 index 000000000..9265c113f --- /dev/null +++ b/tests/corpus/localized-failure-degraded-initializer.ts @@ -0,0 +1,30 @@ +// Localized failure, stage L4, at a forward-captured binding: functions +// declared before a const capture it, and the const's initializer has an +// expression whose type does not compile. The statement lowers again with +// that expression degraded, and it initializes the binding the functions +// captured: before the declaration a call throws ReferenceError, after it +// the call reads the value. +type Hook = Map string> | AbortSignal | (() => number) | Set; +interface Plugin { + name: string; + hook: Hook; +} +function load(text: string): unknown { + return JSON.parse(text); +} +function main(): void { + function nameOf(): string { + return plugin.name; + } + function attempt(): string { + try { + return nameOf(); + } catch (e) { + return e instanceof ReferenceError ? "ReferenceError" : "other"; + } + } + console.log(attempt()); + const plugin = { name: (load('{"name":"tags","hook":null}') as Plugin).name }; + console.log(attempt(), nameOf().length); +} +main(); diff --git a/tests/corpus/localized-failure-expressions.ts b/tests/corpus/localized-failure-expressions.ts new file mode 100644 index 000000000..e41e16fae --- /dev/null +++ b/tests/corpus/localized-failure-expressions.ts @@ -0,0 +1,24 @@ +// Localized failure, stage L4: expressions whose own type does not +// compile (casts, literals with an unmappable contextual type, spread +// clones, temporaries). +type Hook = Map string> | AbortSignal | (() => number) | Set; +interface Plugin { + name: string; + hook: Hook; +} +function load(text: string): unknown { + return JSON.parse(text); +} +function count(p: { name: string }): number { + return p.name.length; +} +const raw = load('{"name":"tags","hook":null}'); +// A cast to a type that does not compile. +console.log(count(raw as Plugin)); +// A literal whose contextual type does not compile, used without a slot. +console.log(count({ name: "lit", hook: () => 1 } as Plugin)); +// A spread clone of such a value. +const base = { name: "base", hook: new Set(["a"]) }; +console.log(count({ ...(base as Plugin), name: "clone" })); +// A temporary: an array literal of them, mapped. +console.log([{ name: "a", hook: () => 2 } as Plugin, { name: "bb", hook: new Set() } as Plugin].map((p) => p.name).join("+")); 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-push-down.ts b/tests/corpus/localized-failure-push-down.ts new file mode 100644 index 000000000..a10a453f8 --- /dev/null +++ b/tests/corpus/localized-failure-push-down.ts @@ -0,0 +1,52 @@ +// Localized failure, stage L3: push-down. A declared type that does not +// compile keeps its compilable shape; only the failing component is a +// checked dynamic piece. +type Hook = Map string> | AbortSignal | (() => number) | Set; + +interface Plugin { + name: string; + priority: number; + hook: Hook; +} + +class Registry { + plugins: Plugin[] = []; + byName = new Map(); + pending: Promise | undefined; + add(p: Plugin): void { + this.plugins.push(p); + this.byName.set(p.name, p); + } + total(): number { + let sum = 0; + for (const p of this.plugins) sum += p.priority; + return sum; + } + names(): string { + return this.plugins.map((p) => p.name).join(","); + } + run(name: string): string { + const p = this.byName.get(name); + if (p === undefined) return "missing"; + const h = p.hook; + return typeof h === "function" ? `${p.name}=${h()}` : h instanceof Set ? `${p.name}#${h.size}` : "other"; + } +} + +type Handler = (event: string, extra: Hook) => number; +function dispatch(handlers: Handler[], event: string): number { + let n = 0; + for (const h of handlers) n += h(event, () => event.length); + return n; +} + +const r = new Registry(); +const counter: Plugin = { name: "counter", priority: 2, hook: () => 41 }; +r.add(counter); +r.add({ name: "tags", priority: 5, hook: new Set(["a", "b"]) }); +console.log(r.total(), r.names(), r.run("counter"), r.run("tags"), r.run("x")); +counter.priority = 10; +console.log(r.total(), r.byName.get("counter") === counter, JSON.stringify(r.plugins.map((p) => [p.name, p.priority]))); +r.pending = Promise.resolve(counter); +r.pending.then((p) => console.log("pending", p.name, p === counter)); +console.log(dispatch([(e, x) => e.length, (e, x) => (typeof x === "function" ? x() * 2 : 0)], "hello")); 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/native-worker-terminate-short-inner-loops.ts b/tests/corpus/native-worker-terminate-short-inner-loops.ts new file mode 100644 index 000000000..33a213ea8 --- /dev/null +++ b/tests/corpus/native-worker-terminate-short-inner-loops.ts @@ -0,0 +1,96 @@ +// worker.terminate() stops a worker that spends its time in short runs of +// inner loops: counted and paired loops of a few iterations inside loops +// without a condition, runs at the length where an inner loop starts to +// poll on its own, and a short inner loop whose body calls a slow function. +// Each terminate resolves promptly. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function flips(n: number): number { + const p = new Int32Array(n), + q = new Int32Array(n), + count = new Int32Array(n); + for (let i = 0; i < n; i++) p[i] = i; + let total = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < n; i++) q[i] = p[i]!; + let first = q[0]!; + for (;;) { + if (first === 0) break; + for (let i = 0, j = first; i < j; i++, j--) { + const t = q[i]!; + q[i] = q[j]!; + q[j] = t; + } + total++; + first = q[0]!; + } + // The next permutation: rotate the first k + 1 elements for the + // smallest k whose count has not run out. + for (let k = 1; ; k++) { + if (k === n) { + for (let i = 0; i < n; i++) p[i] = i; + count.fill(0); + break; + } + const head = p[0]!; + for (let i = 0; i < k; i++) p[i] = p[i + 1]!; + p[k] = head; + if (++count[k]! <= k) break; + count[k] = 0; + } + if (round === 2000) parentPort!.postMessage("ready"); + } + return total; +} + +function thresholds(values: number[]): number { + let acc = 0; + for (let round = 0; ; round++) { + for (const run of [63, 64, 65]) { + for (let i = 0; i < run; i++) acc = (acc + values[i]! * round) % 1000003; + for (let i = 0, j = run; i < j; i++, j--) acc = (acc + values[i]! + values[j]!) % 1000003; + } + if (round === 2000) parentPort!.postMessage("ready"); + } + return acc; +} + +function slow(seed: number): number { + let acc = seed; + for (let i = 0; i < 20000; i++) acc = (acc * 31 + i) % 1000003; + return acc; +} + +function slowBodies(): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < 4; i++) acc = (acc + slow(acc + i)) % 1000003; + if (round === 50) parentPort!.postMessage("ready"); + } + return acc; +} + +if (isMainThread) { + for (const kind of ["flips", "thresholds", "slow"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + const asked = Date.now(); + void worker.terminate().then((code: number) => { + console.log(kind, "terminate", code, Date.now() - asked < 1000 ? "prompt" : "late"); + }); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + let result = 0; + if (workerData === "flips") result = flips(9); + else if (workerData === "thresholds") + result = thresholds(Array.from({ length: 70 }, (_, i) => i)); + else result = slowBodies(); + 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/non-null-element-computed-index.ts b/tests/corpus/non-null-element-computed-index.ts new file mode 100644 index 000000000..4afa873f0 --- /dev/null +++ b/tests/corpus/non-null-element-computed-index.ts @@ -0,0 +1,84 @@ +// Member reads through non-null asserted element reads at computed indexes: +// `xs[xs.length - 1]!`, `xs[i + 1]!`, `xs[-i]!` and indexes read from +// fields. A missing element (past the end, before the start, a fraction, +// NaN, or an empty array) is undefined, so the member access throws Node's +// TypeError; a present one reads the member. + +interface Gene { + c: number; + sc: number; +} + +class Cell { + label: string; + constructor(label: string) { + this.label = label; + } + shout(): string { + return this.label.toUpperCase(); + } +} + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label.padEnd(16), "ok", f()); + } catch (e) { + const error = e as Error; + console.log(label.padEnd(16), error instanceof TypeError, error.message); + } +} + +// The fallback of a cumulative search, as fasta writes it. +function pick(genes: Gene[], rand: number): number { + for (const gene of genes) { + if (rand < gene.c) return gene.sc; + } + return genes[genes.length - 1]!.sc; +} + +const genes: Gene[] = [ + { c: 0.25, sc: 97 }, + { c: 0.5, sc: 99 }, + { c: 0.75, sc: 103 }, +]; +const codes: number[] = []; +for (let k = 0; k < 8; k++) codes.push(pick(genes, k / 8)); +console.log("codes", codes.join(",")); +attempt("fallback", () => pick(genes, 2)); +attempt("empty fallback", () => pick([], 0.5)); + +const cells = [new Cell("a"), new Cell("b"), new Cell("c")]; +const none: Cell[] = []; +const holey: Cell[] = []; +holey[3] = new Cell("d"); +const box = { at: 1, size: 3 }; + +for (const i of [0, 1, 2, 3, -1, 0.5, NaN]) { + attempt(`last - ${i}`, () => cells[cells.length - 1 - i]!.label); + attempt(`next ${i}`, () => cells[i + 1]!.label); + attempt(`neg ${i}`, () => cells[-i]!.label); + attempt(`double ${i}`, () => cells[i * 2]!.shout()); +} +attempt("empty last", () => none[none.length - 1]!.label); +attempt("empty method", () => none[none.length - 1]!.shout()); +attempt("hole last", () => holey[holey.length - 1]!.label); +attempt("hole before", () => holey[holey.length - 2]!.label); +attempt("field index", () => cells[box.at - 1]!.label); +attempt("field past", () => cells[box.size + box.at]!.label); +attempt("other length", () => cells[holey.length - 2]!.label); + +const rows = [ + [1, 2], + [3, 4], +]; +attempt("row last", () => rows[rows.length - 1]![0]); +attempt("row past", () => rows[rows.length]![1]); +attempt("row length", () => rows[rows.length - 2]!.length); + +// The same read in a loop that runs past the end. +let total = 0; +try { + for (let i = 0; i < 5; i++) total += cells[cells.length - 1 - i]!.label.length; +} catch (e) { + console.log("loop", (e as Error).message, total); +} diff --git a/tests/corpus/record-dynamic-writes.ts b/tests/corpus/record-dynamic-writes.ts new file mode 100644 index 000000000..58a47116b --- /dev/null +++ b/tests/corpus/record-dynamic-writes.ts @@ -0,0 +1,62 @@ +// Writes through an untyped view of a native record store into the record +// itself: typed reads see them, the view keeps answering from the record +// (no snapshot), and typed writes show through the view again. +type Point = { x: number; y: number; label: string; tag?: string }; +const p: Point = { x: 1, y: 2, label: "a" }; +const d = p as unknown as Record; +d.x = 10; +d.label = "b"; +console.log(p.x, p.label, JSON.stringify(d), Object.keys(d).join(",")); +p.y = 7; +console.log(d.y, JSON.stringify(p), "y" in d); +d.tag = "t"; +console.log(p.tag, JSON.stringify(d), Object.keys(p).join(",")); +d.tag = "u"; +console.log(p.tag, d.tag); +d.label = "c"; +console.log(p.label, JSON.stringify(Object.entries(d))); + +// Nested values written through the view are the record's own. +type Nested = { inner: { v: number }; list: number[]; flag: boolean }; +const n: Nested = { inner: { v: 1 }, list: [1], flag: false }; +const dn = n as unknown as { inner: unknown; list: unknown; flag: unknown }; +const fresh = { v: 2 }; +dn.inner = fresh; +fresh.v = 3; +console.log(n.inner.v, JSON.stringify(dn)); +dn.list = [4, 5]; +n.list.push(6); +console.log(n.list.length, n.list[1], JSON.stringify(dn.list)); +dn.flag = true; +console.log(n.flag, dn.flag); + +// A counter updated through the view in a loop. +const acc: { total: number; count: number } = { total: 0, count: 0 }; +const da = acc as unknown as { total: number; count: number }; +for (let i = 0; i < 100000; i++) { + da.total = da.total + i; + da.count += 1; +} +console.log(acc.total, acc.count); + +// Object.assign onto the view. +type Cfg = { host: string; port: number; debug: boolean; tags?: string[] }; +const c: Cfg = { host: "a", port: 1, debug: false, tags: ["x"] }; +const dc = c as unknown as object; +Object.assign(dc, { port: 2, debug: true }); +console.log(c.port, c.debug, JSON.stringify(dc)); +Object.assign(dc, { host: "z", extra: 1 }); +console.log(c.host, JSON.stringify(dc), Object.keys(dc).length); +c.port = 3; +console.log(JSON.stringify(dc)); + +// Writes through a function that takes the record as unknown. +function bump(value: unknown, key: string): void { + const object = value as Record; + object[key] = object[key]! + 1; +} +const counts = { a: 1, b: 2 }; +bump(counts, "a"); +bump(counts, "b"); +bump(counts, "b"); +console.log(counts.a, counts.b, JSON.stringify(counts)); diff --git a/tests/corpus/record-prefix-identity.ts b/tests/corpus/record-prefix-identity.ts new file mode 100644 index 000000000..b1747da10 --- /dev/null +++ b/tests/corpus/record-prefix-identity.ts @@ -0,0 +1,52 @@ +// A record value converted from an untyped value into a narrower record +// type whose fields begin the wider record's storage is the same object: +// a write through either name shows through the other, untyped views of +// the narrower name see every property, and identity holds. +type Wide = { a: number; b: string; c: boolean; d: number }; +type Narrow = { a: number; b: string }; +type Head = { a: number }; +function make(): unknown { + return { a: 1, b: "x", c: true, d: 4 } satisfies Wide; +} +const src = make(); +const w = src as Wide; +const n = src as Narrow; +n.a = 10; +console.log(w.a, (src as Head).a); +w.b = "y"; +console.log(n.b); +console.log(JSON.stringify(src), JSON.stringify(n)); +const n2 = src as Narrow; +console.log(n2 === n, n2.a); +(src as Record).a = 30; +console.log(w.a, n.a, (src as Head).a); +console.log(Object.keys(n).join(","), JSON.stringify({ n }), (n as unknown) === src); +console.log(n); + +// A native record boxed and narrowed in a function. +function narrow(value: unknown): Narrow { + return value as Narrow; +} +const wide: Wide = { a: 1, b: "p", c: false, d: 2 }; +const seen = narrow(wide); +seen.b = "q"; +wide.a = 5; +console.log(wide.b, seen.a, JSON.stringify(seen)); + +// Narrowing to a leading field of a record whose other fields are scalars. +type Sample = { a: number; b: number; c: boolean }; +const items: Sample[] = [ + { a: 1, b: 10, c: true }, + { a: 2, b: 20, c: false }, +]; +function asHead(value: unknown): Head { + return value as Head; +} +const heads = items.map((item) => asHead(item)); +for (const head of heads) head.a *= 10; +items[0]!.a += 1; +console.log(JSON.stringify(items), JSON.stringify(heads), heads[0]!.a); +const last = items[items.length - 1]!; +const second = asHead(last); +second.a = 7; +console.log(last.a, (second as unknown) === (last as unknown), Object.keys(second).join(",")); 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/short-char-codes.ts b/tests/corpus/short-char-codes.ts new file mode 100644 index 000000000..14c858bb5 --- /dev/null +++ b/tests/corpus/short-char-codes.ts @@ -0,0 +1,50 @@ +// charCodeAt and codePointAt on short non-ASCII strings: characters of each +// UTF-8 length, astral pairs, lone surrogates, out-of-range and non-integer +// indexes, strings at the 16-byte boundary, and a long receiver scanned in +// between. + +const samples = [ + "é", "ß", "日", "語", "😀", "🚀", "a", "", + "café", "naïve", "straße", "日本語", "テキスト", "한국어", + "a😀b", "😀😀", "xéy日z", + "\ud800", "\udc00", "a\ud800b", "\ud83d", "\ude00x", + "0123456789abcdé", "0123456789abcdeé", "0123456789abcdefé", "ééééééé", "éééééééé", "ééééééééé", +]; + +function codes(s: string): string { + const out: string[] = []; + for (let i = -1; i <= s.length + 1; i++) out.push(String(s.charCodeAt(i))); + out.push(String(s.charCodeAt(0.5)), String(s.charCodeAt(NaN)), String(s.charCodeAt(1.9))); + return out.join(","); +} + +function points(s: string): string { + const out: string[] = []; + for (let i = -1; i <= s.length + 1; i++) out.push(String(s.codePointAt(i))); + return out.join(","); +} + +for (const s of samples) { + console.log(JSON.stringify(s), s.length, codes(s)); + console.log(" points", points(s)); +} + +// A long receiver scanned while short ones are read. +let long = ""; +for (let i = 0; i < 200; i++) long += i % 3 === 0 ? "é" : i % 3 === 1 ? "日" : "x"; +let sum = 0; +for (let i = 0; i < long.length; i++) { + sum += long.charCodeAt(i); + sum += samples[i % samples.length]!.charCodeAt(0) || 0; +} +console.log("long", long.length, sum); + +// Characters of a for-of loop classified by their code points. +let letters = 0; +let others = 0; +for (const c of "the café, 日本 😀 naïve!") { + const code = c.codePointAt(0)!; + if ((code >= 0x61 && code <= 0x7a) || code >= 0xc0) letters++; + else others++; +} +console.log("letters", letters, "others", others); 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/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/string-spread.ts b/tests/corpus/string-spread.ts new file mode 100644 index 000000000..6813806f4 --- /dev/null +++ b/tests/corpus/string-spread.ts @@ -0,0 +1,25 @@ +// Spreading strings into arrays: code points of each UTF-8 length, astral +// pairs, lone surrogates, the empty string, and the fresh array each spread +// returns. + +const samples = ["", "a", "abc", "é", "café", "日本語", "😀", "a😀b", "🚀🚀", "\ud800", "a\udc00b", "xé日😀"]; + +for (const s of samples) { + const chars = [...s]; + console.log(JSON.stringify(s), chars.length, JSON.stringify(chars), chars.join("|")); +} + +// Each spread is a new array: changing one leaves the others and the string. +const text = "héllo 😀"; +const first = [...text]; +const second = [...text]; +first.reverse(); +first.push("!"); +console.log(first.join(""), second.join(""), first === second, text); + +// The spread result is an ordinary array. +const letters = [..."naïve"]; +letters.sort(); +console.log(letters, letters.length, Array.isArray(letters)); +console.log([..."abc"].map((c) => c.toUpperCase()).join("")); +console.log([..."日本"].concat([..."語"]).join("-")); 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/typed-array-nested-loop-versions.ts b/tests/corpus/typed-array-nested-loop-versions.ts new file mode 100644 index 000000000..19e408d30 --- /dev/null +++ b/tests/corpus/typed-array-nested-loop-versions.ts @@ -0,0 +1,68 @@ +// Loops whose own typed-array accesses run once per pass around nested +// loops that read typed arrays: row stores past the end, reads at NaN, +// negative and fractional rows, rows that read a typed array the nested +// loop writes, and a nested loop that reads the row's own result. +function rowSums(u: Float64Array, out: Float64Array, start: number, end: number): void { + for (let i = start; i < end; i++) { + let t = out[i] ?? -1; + for (let j = 0; j < u.length; j++) t += u[j]! * (i + 1); + out[i] = t; + } +} + +function scaled(u: Float64Array, scale: Float64Array, out: Float64Array, rows: number): void { + for (let i = 0; i < rows; i++) { + const s = scale[i * 2] ?? 0.5; + for (let j = 0; j < u.length; j++) out[j] = out[j]! + u[j]! * s; + out[i] = out[i]! - 1; + } +} + +function chained(v: Int32Array, rows: number): number { + let total = 0; + for (let i = 0; i < rows; i++) { + let k = 0; + while (k < v.length) { + v[k] = v[k]! + (v[i] ?? 7); + k++; + } + total += v[i + 1] ?? -3; + } + return total; +} + +const show = (label: string, v: Float64Array | Int32Array): void => { + const parts: string[] = []; + for (let k = 0; k < v.length; k++) parts.push(Object.is(v[k], -0) ? "-0" : String(v[k])); + console.log(label, parts.join(" ")); +}; + +const u = new Float64Array([0.5, -1.25, 3, 7.75]); +for (const [start, end] of [ + [0, 4], + [0, 7], + [2, 9], + [-2, 3], + [0.5, 3.5], + [NaN, 3], + [3, 1], +]) { + const out = new Float64Array(5).fill(2); + rowSums(u, out, start!, end!); + show(`rows ${start} ${end}`, out); +} + +for (const rows of [0, 1, 3, 6]) { + const out = new Float64Array(4).fill(1); + scaled(u, new Float64Array([2, -1, 0.25, 4, 8]), out, rows); + show(`scaled ${rows}`, out); + const same = new Float64Array([1, 2, 3, 4]); + scaled(same, same, same, rows); + show(`aliased ${rows}`, same); +} + +for (const rows of [0, 2, 4, 9]) { + const v = new Int32Array([1, -2, 3, 2147483647]); + const total = chained(v, rows); + show(`chained ${rows} ${total}`, v); +} diff --git a/tests/corpus/typed-array-offset-indexes.ts b/tests/corpus/typed-array-offset-indexes.ts new file mode 100644 index 000000000..2f17d4272 --- /dev/null +++ b/tests/corpus/typed-array-offset-indexes.ts @@ -0,0 +1,52 @@ +// Typed array reads and writes at `base + c`, `c + base` and `base - c`, +// where `base` is a number that may be an integer, a fraction, negative, +// -0, NaN, infinite or past the length. + +function readGroup(values: Float64Array, at: number): number[] { + return [values[at + 0]!, values[at + 1]!, values[2 + at]!, values[at - 1]!, values[at + 7]!]; +} + +function writeGroup(values: Int32Array, at: number, value: number): void { + values[at + 0] = value; + values[1 + at] = value + 1; + values[at - 2] = value - 2; +} + +function sumWindow(values: Uint8Array, at: number): number { + return (values[at] ?? 0) + (values[at + 1] ?? 0) + (values[at + 2] ?? 0) + (values[at + 3] ?? 0); +} + +const floats = new Float64Array(16); +for (let i = 0; i < floats.length; i++) floats[i] = i * 1.5; +const bases = [0, 1, 4, 8.5, -0, -1, -0.5, 15, 16, 2 ** 31, 2 ** 52, 2 ** 53, -(2 ** 53), NaN, Infinity, -Infinity]; +for (const base of bases) console.log(String(base), JSON.stringify(readGroup(floats, base))); + +const ints = new Int32Array(8); +for (const base of [2, 3.25, -0, 7, 9, NaN, 2 ** 52 + 1]) { + writeGroup(ints, base, 10); + console.log(String(base), ints.join(",")); +} + +const bytes = new Uint8Array([5, 4, 3, 2, 1, 250, 251, 252]); +let total = 0; +for (let i = 0; i < 8; i++) total += sumWindow(bytes, i); +console.log(total, sumWindow(bytes, 6), sumWindow(bytes, 4.5)); + +// A base with a fraction too small to survive the sum indexes like the sum. +for (const base of [1e-20, -1e-20, 2 ** -60, 3 + 2 ** -50, 0.5, -0.75]) { + console.log(String(base), JSON.stringify(readGroup(floats, base))); + writeGroup(ints, base, 20); + console.log(String(base), ints.join(",")); +} + +// A right-hand side that changes the base: the index is the one evaluated +// before it. +function rebase(values: Int32Array, at: number): number { + values[at + 1] = ((at = 5), 31); + values[2 + at] = at++; + values[at - 1] += ((at = 0), 4); + Atomics.add(values, at + 3, ((at = 6), 2)); + return at; +} +const targets = new Int32Array(10); +console.log(rebase(targets, 2), targets.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-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-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/untyped-nullish-scalar-defaults.mjs b/tests/corpus/untyped-nullish-scalar-defaults.mjs new file mode 100644 index 000000000..507945456 --- /dev/null +++ b/tests/corpus/untyped-nullish-scalar-defaults.mjs @@ -0,0 +1,50 @@ +// `??` in untyped JavaScript where the left is an optional number, boolean or +// string and the default has the same type: Map reads, find(), a function that +// may return undefined, and defaults of another type, null or an object. +const words = ["alpha", "beta", "gamma", "beta", "delta", "beta", "alpha"]; +const counts = new Map(); +for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1); +for (const [word, n] of counts) console.log(word, n); + +const odd = new Map(); +odd.set("nan", NaN); +odd.set("zero", -0); +odd.set("one", 1); +for (const key of ["nan", "zero", "one", "missing"]) { + const value = odd.get(key) ?? 7; + console.log(key, value, Object.is(value, -0), Number.isNaN(value), typeof value); +} + +const flags = new Map(); +flags.set("on", true); +flags.set("off", false); +for (const key of ["on", "off", "missing"]) console.log(key, flags.get(key) ?? true); + +const names = new Map(); +names.set("a", ""); +names.set("b", "bee"); +for (const key of ["a", "b", "c"]) console.log(JSON.stringify(names.get(key) ?? "none")); + +const values = [3, 8, 13, 21]; +console.log(values.find((v) => v > 10) ?? -1, values.find((v) => v > 100) ?? -1); + +function lookup(table, key) { + return key in table ? table[key] : undefined; +} +const table = { x: 1.5, y: 0 }; +console.log(lookup(table, "x") ?? 0, lookup(table, "y") ?? 9, lookup(table, "z") ?? 9); + +// Defaults of another type keep the left's value or take the default as is. +console.log(counts.get("beta") ?? "none", counts.get("omega") ?? "none"); +console.log(counts.get("omega") ?? null, counts.get("beta") ?? null); +const fallback = { n: 0 }; +console.log(counts.get("omega") ?? fallback, counts.get("alpha") ?? fallback); + +// The result is an ordinary value in later arithmetic and comparisons. +let total = 0; +for (const word of ["alpha", "beta", "omega"]) total += (counts.get(word) ?? 0) * 10; +console.log(total, (counts.get("omega") ?? 0) === 0, (odd.get("nan") ?? 0) === (odd.get("nan") ?? 0)); + +// A default of the same type that can itself be undefined keeps its value. +const spare = [4, 5]; +console.log(counts.get("omega") ?? spare[9], counts.get("beta") ?? spare[9], counts.get("omega") ?? spare[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/while-entry-range-versions.ts b/tests/corpus/while-entry-range-versions.ts new file mode 100644 index 000000000..7d96c243a --- /dev/null +++ b/tests/corpus/while-entry-range-versions.ts @@ -0,0 +1,160 @@ +// While loops whose remainder reads locals copied from module globals, +// parameters and fields. Entry values cover integers inside and outside +// [-2^48, 2^48], fractions, -0, NaN and infinities. + +let column = 0; +let lines = 0; + +// A switch with more cases than a small leaf function has statements. +function complement(code: number): number { + switch (code) { + case 65: + return 84; + case 67: + return 71; + case 71: + return 67; + case 84: + return 65; + case 97: + return 116; + case 99: + return 103; + case 103: + return 99; + case 116: + return 97; + case 85: + return 65; + case 117: + return 97; + case 82: + return 89; + case 89: + return 82; + case 75: + return 77; + case 77: + return 75; + } + return 78; +} + +// Copies bytes in reverse, complemented, with a newline after every 8 +// columns counted from the module-level column and line counters. +function wrap(input: Uint8Array, out: Uint8Array): number { + const base = column; + let breaks = lines; + let count = 0; + let o = 0; + let i = 0; + const len = input.length; + let last = 1; + let negativeZeros = 0; + while (i < len) { + out[o] = complement(input[len - 1 - i]!); + i++; + o++; + count++; + last = (count + base - breaks) % 8; + if (Object.is(last, -0)) negativeZeros++; + if (last === 0) { + out[o] = 10; + o++; + count++; + breaks++; + } + } + column = base + count; + lines = breaks; + console.log(` last ${Object.is(last, -0) ? "-0" : last}, ${negativeZeros} of them -0`); + return o; +} + +function text(out: Uint8Array, n: number): string { + let s = ""; + for (let i = 0; i < n; i++) s += out[i] === 10 ? "|" : String.fromCharCode(out[i]!); + return s; +} + +const input = new Uint8Array(29); +const bases = "ACGTTGCAAGCTTCGA"; +for (let i = 0; i < input.length; i++) input[i] = bases.charCodeAt((i * 7) % bases.length); +const out = new Uint8Array(64); + +const starts: [number, number][] = [ + [0, 0], + [5, 2], + [-13, 0], + [-16, 0], + [2 ** 48, 3], + [-(2 ** 48), 0], + [2 ** 48 + 1, 0], + [2 ** 50, 1], + [2 ** 53 - 5, 0], + [2 ** 53, 0], + [2 ** 53 + 2, 1], + [0.5, 0], + [3, 0.25], + [3, 2 ** 49], + [-0, 0], + [0, -0], + [NaN, 0], + [Infinity, 0], + [-Infinity, 0], + [1e300, 0], +]; +for (const [c, l] of starts) { + column = c; + lines = l; + console.log(`start column ${c} lines ${l}`); + const n = wrap(input, out); + console.log(` ${text(out, n)}`); + console.log(` column ${column} lines ${lines}`); +} + +// Two calls in a row: the second starts from the first one's counters. +column = 3; +lines = 0; +wrap(input, out); +const n = wrap(input, out); +console.log(`again ${text(out, n)} column ${column} lines ${lines}`); + +// A parameter and a field as the loop's entry values. +function ring(values: Int32Array, offset: number, size: number): number { + let sum = 0; + let i = 0; + while (i < values.length) { + sum += values[(i + offset) % size]!; + i++; + } + return sum; +} +const values = new Int32Array([3, 1, 4, 1, 5, 9, 2, 6]); +for (const offset of [0, 3, 7, 2 ** 40, 2 ** 49, 1.5, -0, -2]) { + const sum = ring(values, offset, values.length); + console.log(`ring ${offset} ${Number.isNaN(sum) ? "NaN" : sum}`); +} + +class Cursor { + position: number; + constructor(position: number) { + this.position = position; + } + stripes(bytes: Uint8Array): string { + const start = this.position; + let i = 0; + let marks = ""; + while (i < bytes.length) { + const r = (start + i) % 3; + marks += Object.is(r, -0) ? "z" : String(r); + i++; + } + this.position = start + i; + return marks; + } +} +for (const position of [0, 4, -5, 2 ** 48 + 7, 0.75, 2 ** 60]) { + const cursor = new Cursor(position); + console.log(`stripes ${position} ${cursor.stripes(input.subarray(0, 7))} ${cursor.position}`); +} diff --git a/tests/corpus/width-layout-identity.ts b/tests/corpus/width-layout-identity.ts new file mode 100644 index 000000000..fcae6faac --- /dev/null +++ b/tests/corpus/width-layout-identity.ts @@ -0,0 +1,68 @@ +// An array or record passed to a parameter, returned, or bound to a const +// under a type that lacks some of its optional fields stays the same value: +// identity comparisons and writes through any of the names see one object. + +interface Entry { + key: string; + hits: number; + note?: string; +} +type Counted = { key: string; hits: number }; + +function same(list: Counted[], other: Entry[]): boolean { + return (list as unknown) === other; +} +function keep(list: Counted[]): Counted[] { + return list; +} +function first(list: Counted[]): Counted { + return list[0]!; +} +function grow(list: Counted[], key: string): Counted[] { + list.push({ key, hits: 0 }); + return list; +} + +const entries: Entry[] = [{ key: "a", hits: 1, note: "first" }]; +console.log(same(entries, entries), keep(entries) === entries, first(entries) === entries[0]); +const grown = grow(entries, "b"); +grown.push({ key: "c", hits: 2 }); +console.log(grown === entries, entries.length, entries.map((e) => e.key).join(",")); + +function local(list: Entry[]): boolean { + const view: Counted[] = list; + const kept = keep(list); + view[0]!.hits += 10; + return view === list && kept === list; +} +console.log(local(entries), entries[0]!.hits, entries[0]!.note); + +// An annotated const that kept its initializer's layout is that value when it +// is passed on: the callee's writes reach both names. +function track(list: Counted[], key: string): void { + list.push({ key, hits: 0 }); +} +function viaAlias(): string { + const source: Entry[] = [{ key: "s", hits: 1, note: "kept" }]; + const view: Counted[] = source; + track(view, "t"); + return `${view.length} ${source.length} ${view === source}`; +} +console.log(viaAlias()); +const moduleSource: Entry[] = [{ key: "m", hits: 1 }]; +const moduleView: Counted[] = moduleSource; +track(moduleView, "n"); +console.log(moduleView.length, moduleSource.length, moduleView === moduleSource); + +// The same function body lowered for a wider argument and for its own type: +// each lowering passes its own binding on. +function addTwice(list: Counted[], key: string): number { + const alias: Counted[] = list; + track(alias, key); + track(alias, `${key}2`); + return list.length; +} +const wideList: Entry[] = [{ key: "w", hits: 1, note: "n" }]; +const plainList: Counted[] = [{ key: "p", hits: 1 }]; +console.log(addTwice(wideList, "a"), addTwice(plainList, "b"), addTwice(wideList, "c")); +console.log(wideList.length, plainList.length); diff --git a/tests/corpus/width-layout-parameter-writes.ts b/tests/corpus/width-layout-parameter-writes.ts new file mode 100644 index 000000000..07d474352 --- /dev/null +++ b/tests/corpus/width-layout-parameter-writes.ts @@ -0,0 +1,96 @@ +// An array or record passed to a parameter whose type lacks some of its +// optional fields is the caller's value inside the callee: pushes, index +// stores, element-field writes and removals through the parameter reach the +// caller's array, as in Node. + +interface Param { + id: string; + name: string; + owned?: true; +} +type Slot = { id: string; name: string }; + +function addSlot(params: Slot[], name: string): void { + params.push({ id: `${name}.0`, name }); +} +function rename(params: Slot[], index: number, name: string): void { + params[index]!.name = name; +} +function replace(params: Slot[], index: number, slot: Slot): void { + params[index] = slot; +} +function removeFirst(params: Slot[]): string | undefined { + return params.shift()?.name; +} +function forward(params: Slot[], name: string): void { + addSlot(params, name); + addSlot(params, `${name}2`); +} +function names(params: Slot[]): string { + return params.map((p) => p.name).join(","); +} + +const params: Param[] = [{ id: "a.0", name: "a", owned: true }]; +addSlot(params, "b"); +rename(params, 0, "first"); +replace(params, 1, { id: "z.0", name: "z" }); +forward(params, "c"); +console.log(params.length, JSON.stringify(params)); +console.log(removeFirst(params), params.length, params[0]?.owned, names(params)); +params.splice(1, 1); +console.log(names(params)); + +const holder = { list: params }; +addSlot(holder.list, "d"); +console.log(holder.list.length, params.length, holder.list === params); + +// A record parameter: writes to its fields reach the caller's record, and the +// optional field the parameter's type lacks stays as the caller left it. +interface Counter { + label: string; + hits: number; + note?: string; +} +function bump(target: { label: string; hits: number }): void { + target.hits += 1; + target.label = target.label.toUpperCase(); +} +const counter: Counter = { label: "c", hits: 1 }; +const noted: Counter = { label: "n", hits: 0, note: "kept" }; +bump(counter); +bump(counter); +bump(noted); +console.log(JSON.stringify(counter), "note" in counter, JSON.stringify(noted)); + +// Recursion through the same parameter. +function fill(params: Slot[], n: number): void { + if (n === 0) return; + addSlot(params, `r${n}`); + fill(params, n - 1); +} +const deep: Param[] = []; +fill(deep, 3); +console.log(names(deep), deep.length); + +// Recursive layouts: a tree with an optional field passed where a tree +// without it is expected. +interface Tree { + label: string; + kids: Tree[]; + note?: string; +} +interface Plain { + label: string; + kids: Plain[]; +} +function addKid(t: Plain, label: string): void { + t.kids.push({ label, kids: [] }); +} +function size(t: Plain): number { + let n = 1; + for (const k of t.kids) n += size(k); + return n; +} +const tree: Tree = { label: "r", kids: [{ label: "a", kids: [], note: "leaf" }] }; +addKid(tree, "b"); +console.log(size(tree), tree.kids.map((k) => k.label).join(","), tree.kids[0]!.note); diff --git a/tests/coverage-fixtures/dynamic-slot-hot.ts b/tests/coverage-fixtures/dynamic-slot-hot.ts new file mode 100644 index 000000000..65672ffc1 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-slot-hot.ts @@ -0,0 +1,27 @@ +// A dynamic slot read in a loop: SC6101 names the slot, and coverage +// counts the dynamic operations on it. +type Hook = (() => number) | AbortSignal | Map string>; + +interface Plugin { + name: string; + hook: Hook; +} + +const plugins: Plugin[] = [ + { name: "one", hook: () => 1 }, + { name: "two", hook: () => 2 }, +]; + +function sum(rounds: number): number { + let total = 0; + for (let i = 0; i < rounds; i++) { + for (const p of plugins) { + const hook = p.hook; + if (typeof hook === "function") total += hook(); + else total += (hook as { size?: number }).size ?? 0; + } + } + return total; +} + +console.log(sum(10)); diff --git a/tests/coverage-fixtures/dynamic-slot-same-type.ts b/tests/coverage-fixtures/dynamic-slot-same-type.ts new file mode 100644 index 000000000..61954c6f6 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-slot-same-type.ts @@ -0,0 +1,16 @@ +// Two dynamic slots of one declared type: the operations in the loop are +// on `hot`, and `cold` is read once. +type Hook = (() => number) | AbortSignal | Map string>; + +let cold: Hook = () => 1; +let hot: Hook = () => 2; + +function run(rounds: number): number { + let total = 0; + for (let i = 0; i < rounds; i++) { + if (typeof hot === "function") total += hot(); + } + return total; +} + +console.log(run(10), typeof cold); diff --git a/tests/coverage-fixtures/dynamic-slot-shared.ts b/tests/coverage-fixtures/dynamic-slot-shared.ts new file mode 100644 index 000000000..21624a479 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-slot-shared.ts @@ -0,0 +1,23 @@ +// A type that degrades as a piece of a container (`Plugin.hook`) and as a +// slot of its own (`current`): the operations on `current` belong to +// `current`, not to the container. +type Hook = (() => number) | AbortSignal | Map string>; + +interface Plugin { + name: string; + hook: Hook; +} + +const plugins: Plugin[] = [{ name: "one", hook: () => 1 }]; + +let current: Hook = () => 2; + +function run(rounds: number): number { + let total = 0; + for (let i = 0; i < rounds; i++) { + if (typeof current === "function") total += current(); + } + return total; +} + +console.log(run(10), plugins.length); 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/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/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/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/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/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 0d90e8cac..d94c7c19a 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -108,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/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/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/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/harness/__snapshots__/array-subclass-fences.ts.txt b/tests/harness/__snapshots__/array-subclass-fences.ts.txt new file mode 100644 index 000000000..97e7bdc8d --- /dev/null +++ b/tests/harness/__snapshots__/array-subclass-fences.ts.txt @@ -0,0 +1,55 @@ +array-subclass-fences.ts:5:22 - error SC1090: a generic class that extends Array is not supported yet + + 4 | // are its own instances. + 5 | class Bag extends Array { + | ^~~~~~~~ + 6 | firstOr(fallback: T): T { + +array-subclass-fences.ts:12:23 - error SC1090: extending 'Base', a class that extends Array (one level of Array subclass compiles) is not supported yet + + 11 | class Base extends Array {} + 12 | class Derived extends Base {} + | ^~~~ + 13 | + +array-subclass-fences.ts:15:3 - error SC1090: Symbol.species on a class that extends Array (its methods' results construct the class itself) is not supported yet + + 14 | class Speciated extends Array { + 15 | static get [Symbol.species](): ArrayConstructor { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | return Array; + +array-subclass-fences.ts:20:22 - error SC1090: classes that extend an Array of their own instances are not supported yet + + 19 | + 20 | class Nested extends Array {} + | ^~~~~~~~~~~~~~~~~~~~ + 21 | + +array-subclass-fences.ts:22:13 - error SC1090: constructing values other than classes declared in the program is not supported yet + + 21 | + 22 | console.log(new Bag().firstOr(1)); + | ^~~~~~~~~~~~~~~~~ + 23 | console.log(new Derived().length); + +array-subclass-fences.ts:23:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet + + 22 | console.log(new Bag().firstOr(1)); + 23 | console.log(new Derived().length); + | ^~~~~~~~~~~~~ + 24 | console.log(new Speciated().length); + +array-subclass-fences.ts:24:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet + + 23 | console.log(new Derived().length); + 24 | console.log(new Speciated().length); + | ^~~~~~~~~~~~~~~ + 25 | console.log(new Nested().length); + +array-subclass-fences.ts:25:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet + + 24 | console.log(new Speciated().length); + 25 | console.log(new Nested().length); + | ^~~~~~~~~~~~ + 26 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/array-subclass-retyped-members.ts.txt b/tests/harness/__snapshots__/array-subclass-retyped-members.ts.txt new file mode 100644 index 000000000..6965bfac2 --- /dev/null +++ b/tests/harness/__snapshots__/array-subclass-retyped-members.ts.txt @@ -0,0 +1,20 @@ +array-subclass-retyped-members.ts:21:3 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet + + 20 | if (numbers instanceof Group) { + 21 | numbers.owner = new Holder(); + | ^~~~~~~ + 22 | console.log(numbers.owner.tag); + +array-subclass-retyped-members.ts:22:15 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet + + 21 | numbers.owner = new Holder(); + 22 | console.log(numbers.owner.tag); + | ^~~~~~~ + 23 | console.log(numbers.firstTag()); + +array-subclass-retyped-members.ts:23:15 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet + + 22 | console.log(numbers.owner.tag); + 23 | console.log(numbers.firstTag()); + | ^~~~~~~ + 24 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-explain-localized-failure.txt b/tests/harness/__snapshots__/coverage-explain-localized-failure.txt new file mode 100644 index 000000000..9780a528e --- /dev/null +++ b/tests/harness/__snapshots__/coverage-explain-localized-failure.txt @@ -0,0 +1,16 @@ + dynamic tier (tier assignment, report-only; --tier=auto) + 2 functions already operate on dynamic values (2 statements) + Base.constructor tests/coverage-fixtures/localized-failure.ts:57:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:58:16 + Holder.constructor tests/coverage-fixtures/localized-failure.ts:28:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:29:16 + + 2 demotion requests (refusals and fences the dynamic tier would replace) + + 0 would-be dynamic slots (localized failure, report-only: type failures a checked dynamic slot would hold) + 2 errors: 0 clear with declaration slots (L1-L2), 0 with member and expression slots too (L3-L4); 1 cascade attributed; 1 type failure stays refused + class Cache tests/coverage-fixtures/localized-failure.ts:42:1 (1 request) + <- SC1090 reflection at tests/coverage-fixtures/localized-failure.ts:53:22: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet + -> function turns dynamic + field Cache.tokens tests/coverage-fixtures/localized-failure.ts:43:3 (1 request) + <- SC2020 operation at tests/coverage-fixtures/localized-failure.ts:43:3: 'WeakRef' is part of the standard library types but has no scriptc lowering yet + -> function turns dynamic + function tests/coverage-fixtures/localized-failure.ts:1:1 (2 sites inside) \ No newline at end of file diff --git a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt index 159e88f76..ab325aaaa 100644 --- a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt +++ b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt @@ -48,11 +48,4 @@ maybe-promise-fences.ts:27:50 - error SC2020: 'Promise.resolve over 'Promise | string>) + + 3 | async function f(xs: V[]): Promise { + 4 | const values = await Promise.all(xs); + | ^~ + 5 | values[0]; + + hint: an array of promises (Promise[]) is the supported form \ No newline at end of file diff --git a/tests/harness/__snapshots__/untyped-map-into-interface.ts.txt b/tests/harness/__snapshots__/untyped-map-into-interface.ts.txt new file mode 100644 index 000000000..03b54a136 --- /dev/null +++ b/tests/harness/__snapshots__/untyped-map-into-interface.ts.txt @@ -0,0 +1,8 @@ +untyped-map-into-interface.ts:9:10 - error SC2002: record shapes must match exactly or width-coerce: expected '{ get: (string) => number | undefined; set: (string, number) => unknown }', got 'Map' + + 8 | function make(c?: Cache): Cache { + 9 | return c ?? new Map(); + | ^~~~~~~~~~~~~~ + 10 | } + + hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead \ No newline at end of file diff --git a/tests/harness/bootstrap-seed.test.ts b/tests/harness/bootstrap-seed.test.ts index 76b1f6de5..2da27baea 100644 --- a/tests/harness/bootstrap-seed.test.ts +++ b/tests/harness/bootstrap-seed.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { load } from "js-yaml"; import { expect, test } from "vitest"; -import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs"; +import { seedBuildEnvironment, verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs"; test("bootstrap artifacts require a complete seed from the same revision, lane, and path", () => { const directory = mkdtempSync(join(tmpdir(), "scriptc-seed-contract-")); @@ -36,6 +36,22 @@ test("bootstrap artifacts require a complete seed from the same revision, lane, } }); +test("seeds build without the function layout in both lanes, shared or not", () => { + for (const sanitize of [false, true]) + expect(seedBuildEnvironment(sanitize)).toEqual({ + SCRIPTC_SAN: sanitize ? "1" : "", + SCRIPTC_NATIVE_EMIT_IR: "1", + SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release", + SCRIPTC_NATIVE_LAYOUT: "", + }); + // The bootstrap test builds its own seed when CI shares none. + const driver = readFileSync( + new URL("./self-hosting-native-driver.test.ts", import.meta.url), + "utf8", + ); + expect(driver).toContain("...seedBuildEnvironment(sanitize)"); +}); + test("CI restores each seed only into its matching instrumented bootstrap lane", () => { const workflow = load( readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), diff --git a/tests/harness/ci-lanes.test.ts b/tests/harness/ci-lanes.test.ts index faca8a7d0..3ea51293f 100644 --- a/tests/harness/ci-lanes.test.ts +++ b/tests/harness/ci-lanes.test.ts @@ -166,6 +166,7 @@ test("every job on a macOS runner is in the macos lane", () => { ); expect(macosJobs.map(([name]) => name).sort()).toEqual([ "llvm_artifacts_macos_arm64", + "llvm_artifacts_macos_arm64_contracts", "llvm_artifacts_macos_x64", "mobile_runtime_packs_ios", "native_macos", diff --git a/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json b/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json new file mode 100644 index 000000000..e053c13c8 --- /dev/null +++ b/tests/harness/corpus-costs/7393-non-null-element-operands.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3800 } diff --git a/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json b/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json new file mode 100644 index 000000000..e0d5a6a6d --- /dev/null +++ b/tests/harness/corpus-costs/7395-non-null-element-dereference.ts.json @@ -0,0 +1 @@ +{ "plain": 3585, "san": 5768 } diff --git a/tests/harness/corpus-costs/7517-stream-kept-callbacks.ts.json b/tests/harness/corpus-costs/7517-stream-kept-callbacks.ts.json new file mode 100644 index 000000000..ad7424fd8 --- /dev/null +++ b/tests/harness/corpus-costs/7517-stream-kept-callbacks.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4300 } diff --git a/tests/harness/corpus-costs/7518-stream-dropped-callbacks.ts.json b/tests/harness/corpus-costs/7518-stream-dropped-callbacks.ts.json new file mode 100644 index 000000000..db9671e02 --- /dev/null +++ b/tests/harness/corpus-costs/7518-stream-dropped-callbacks.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 5700 } diff --git a/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json b/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7721-array-subclass-species.ts.json b/tests/harness/corpus-costs/7721-array-subclass-species.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7721-array-subclass-species.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json b/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json b/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json b/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json b/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json b/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7728-array-subclass-species-order.ts.json b/tests/harness/corpus-costs/7728-array-subclass-species-order.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7728-array-subclass-species-order.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7730-array-subclass-retyped-trace.ts.json b/tests/harness/corpus-costs/7730-array-subclass-retyped-trace.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7730-array-subclass-retyped-trace.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7731-array-subclass-deep-release.ts.json b/tests/harness/corpus-costs/7731-array-subclass-deep-release.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7731-array-subclass-deep-release.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7732-array-subclass-species-bound.ts.json b/tests/harness/corpus-costs/7732-array-subclass-species-bound.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7732-array-subclass-species-bound.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/7733-array-subclass-optional-length.ts.json b/tests/harness/corpus-costs/7733-array-subclass-optional-length.ts.json new file mode 100644 index 000000000..a663de213 --- /dev/null +++ b/tests/harness/corpus-costs/7733-array-subclass-optional-length.ts.json @@ -0,0 +1 @@ +{ "plain": 11000, "san": 9000 } diff --git a/tests/harness/corpus-costs/8014-foreign-element-callbacks.ts.json b/tests/harness/corpus-costs/8014-foreign-element-callbacks.ts.json new file mode 100644 index 000000000..981bcbb55 --- /dev/null +++ b/tests/harness/corpus-costs/8014-foreign-element-callbacks.ts.json @@ -0,0 +1 @@ +{ "plain": 5089, "san": 8118 } diff --git a/tests/harness/corpus-costs/8161-global-computed-reads.cjs.json b/tests/harness/corpus-costs/8161-global-computed-reads.cjs.json new file mode 100644 index 000000000..24129b327 --- /dev/null +++ b/tests/harness/corpus-costs/8161-global-computed-reads.cjs.json @@ -0,0 +1 @@ +{ "plain": 3708, "san": 12618 } diff --git a/tests/harness/corpus-costs/8162-navigator-global.cjs.json b/tests/harness/corpus-costs/8162-navigator-global.cjs.json new file mode 100644 index 000000000..433a92b5d --- /dev/null +++ b/tests/harness/corpus-costs/8162-navigator-global.cjs.json @@ -0,0 +1 @@ +{ "plain": 3686, "san": 6899 } diff --git a/tests/harness/corpus-costs/8163-data-function-values.cjs.json b/tests/harness/corpus-costs/8163-data-function-values.cjs.json new file mode 100644 index 000000000..5c1fdff5e --- /dev/null +++ b/tests/harness/corpus-costs/8163-data-function-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 3362, "san": 7680 } diff --git a/tests/harness/corpus-costs/8164-atob-validation.cjs.json b/tests/harness/corpus-costs/8164-atob-validation.cjs.json new file mode 100644 index 000000000..5fd135960 --- /dev/null +++ b/tests/harness/corpus-costs/8164-atob-validation.cjs.json @@ -0,0 +1 @@ +{ "plain": 3065, "san": 4575 } diff --git a/tests/harness/corpus-costs/8168-navigator-language-values.cjs.json b/tests/harness/corpus-costs/8168-navigator-language-values.cjs.json new file mode 100644 index 000000000..e86b2692e --- /dev/null +++ b/tests/harness/corpus-costs/8168-navigator-language-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 3104, "san": 4714 } diff --git a/tests/harness/corpus-costs/8326-never-promise-instanceof.ts.json b/tests/harness/corpus-costs/8326-never-promise-instanceof.ts.json new file mode 100644 index 000000000..4c7ad07f1 --- /dev/null +++ b/tests/harness/corpus-costs/8326-never-promise-instanceof.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 5500 } diff --git a/tests/harness/corpus-costs/8332-worker-shared-counted-loops.ts.json b/tests/harness/corpus-costs/8332-worker-shared-counted-loops.ts.json new file mode 100644 index 000000000..6b0f0395b --- /dev/null +++ b/tests/harness/corpus-costs/8332-worker-shared-counted-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4011, "san": 2208 } diff --git a/tests/harness/corpus-costs/8333-array-subclass-view-writes.ts.json b/tests/harness/corpus-costs/8333-array-subclass-view-writes.ts.json new file mode 100644 index 000000000..37862a1e5 --- /dev/null +++ b/tests/harness/corpus-costs/8333-array-subclass-view-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 2527, "san": 3409 } diff --git a/tests/harness/corpus-costs/8334-array-subclass-inspect-depth.ts.json b/tests/harness/corpus-costs/8334-array-subclass-inspect-depth.ts.json new file mode 100644 index 000000000..8bf64df9a --- /dev/null +++ b/tests/harness/corpus-costs/8334-array-subclass-inspect-depth.ts.json @@ -0,0 +1 @@ +{ "plain": 2468, "san": 3577 } diff --git a/tests/harness/corpus-costs/8370-stack-overflow-cycles/main.ts.json b/tests/harness/corpus-costs/8370-stack-overflow-cycles/main.ts.json new file mode 100644 index 000000000..29769db15 --- /dev/null +++ b/tests/harness/corpus-costs/8370-stack-overflow-cycles/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2780, "san": 53539 } diff --git a/tests/harness/corpus-costs/8405-zlib-compressed-bytes.ts.json b/tests/harness/corpus-costs/8405-zlib-compressed-bytes.ts.json new file mode 100644 index 000000000..9c291a72c --- /dev/null +++ b/tests/harness/corpus-costs/8405-zlib-compressed-bytes.ts.json @@ -0,0 +1 @@ +{ "plain": 5000, "san": 3200 } diff --git a/tests/harness/corpus-costs/8406-zlib-inflate-chunks.cjs.json b/tests/harness/corpus-costs/8406-zlib-inflate-chunks.cjs.json new file mode 100644 index 000000000..2202e2f02 --- /dev/null +++ b/tests/harness/corpus-costs/8406-zlib-inflate-chunks.cjs.json @@ -0,0 +1 @@ +{ "plain": 9000, "san": 13300 } diff --git a/tests/harness/corpus-costs/8407-string-index-of-from-ascii.ts.json b/tests/harness/corpus-costs/8407-string-index-of-from-ascii.ts.json new file mode 100644 index 000000000..054b3c2b4 --- /dev/null +++ b/tests/harness/corpus-costs/8407-string-index-of-from-ascii.ts.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 5000 } diff --git a/tests/harness/corpus-costs/arena-array-subclass.ts.json b/tests/harness/corpus-costs/arena-array-subclass.ts.json new file mode 100644 index 000000000..a4a33f447 --- /dev/null +++ b/tests/harness/corpus-costs/arena-array-subclass.ts.json @@ -0,0 +1 @@ +{ "plain": 3100 } diff --git a/tests/harness/corpus-costs/arena-workers.ts.json b/tests/harness/corpus-costs/arena-workers.ts.json new file mode 100644 index 000000000..2180ff94b --- /dev/null +++ b/tests/harness/corpus-costs/arena-workers.ts.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 18500 } diff --git a/tests/harness/corpus-costs/array-prototype-call-native-arrays.ts.json b/tests/harness/corpus-costs/array-prototype-call-native-arrays.ts.json new file mode 100644 index 000000000..d88973daa --- /dev/null +++ b/tests/harness/corpus-costs/array-prototype-call-native-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 5514, "san": 7205 } diff --git a/tests/harness/corpus-costs/array-subclass-element-cycles.ts.json b/tests/harness/corpus-costs/array-subclass-element-cycles.ts.json new file mode 100644 index 000000000..371d0c7d1 --- /dev/null +++ b/tests/harness/corpus-costs/array-subclass-element-cycles.ts.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 4800 } diff --git a/tests/harness/corpus-costs/array-subclass-element-instanceof.ts.json b/tests/harness/corpus-costs/array-subclass-element-instanceof.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/array-subclass-element-instanceof.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } diff --git a/tests/harness/corpus-costs/constant-choice-chains.ts.json b/tests/harness/corpus-costs/constant-choice-chains.ts.json new file mode 100644 index 000000000..6562c9db3 --- /dev/null +++ b/tests/harness/corpus-costs/constant-choice-chains.ts.json @@ -0,0 +1 @@ +{ "plain": 3921, "san": 5592 } diff --git a/tests/harness/corpus-costs/discarded-logical-void-conditionals.mjs.json b/tests/harness/corpus-costs/discarded-logical-void-conditionals.mjs.json new file mode 100644 index 000000000..3b70bc600 --- /dev/null +++ b/tests/harness/corpus-costs/discarded-logical-void-conditionals.mjs.json @@ -0,0 +1 @@ +{ "plain": 4136, "san": 4906 } diff --git a/tests/harness/corpus-costs/discarded-logical-void-conditionals.ts.json b/tests/harness/corpus-costs/discarded-logical-void-conditionals.ts.json new file mode 100644 index 000000000..85c20d025 --- /dev/null +++ b/tests/harness/corpus-costs/discarded-logical-void-conditionals.ts.json @@ -0,0 +1 @@ +{ "plain": 3559, "san": 5110 } diff --git a/tests/harness/corpus-costs/dyn-cast-identity-fresh.ts.json b/tests/harness/corpus-costs/dyn-cast-identity-fresh.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/dyn-cast-identity-fresh.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/dyn-cast-identity-holders.ts.json b/tests/harness/corpus-costs/dyn-cast-identity-holders.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/dyn-cast-identity-holders.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/dyn-cast-identity-workers.ts.json b/tests/harness/corpus-costs/dyn-cast-identity-workers.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/dyn-cast-identity-workers.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/dyn-cast-identity.ts.json b/tests/harness/corpus-costs/dyn-cast-identity.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/dyn-cast-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/dyn-literal-identity.ts.json b/tests/harness/corpus-costs/dyn-literal-identity.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/dyn-literal-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/frozen-record-untyped-writes.ts.json b/tests/harness/corpus-costs/frozen-record-untyped-writes.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/frozen-record-untyped-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/frozen-untyped-writes-sloppy.cjs.json b/tests/harness/corpus-costs/frozen-untyped-writes-sloppy.cjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/frozen-untyped-writes-sloppy.cjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/fs-readdir-recursive.ts.json b/tests/harness/corpus-costs/fs-readdir-recursive.ts.json new file mode 100644 index 000000000..ad3232154 --- /dev/null +++ b/tests/harness/corpus-costs/fs-readdir-recursive.ts.json @@ -0,0 +1 @@ +{ "plain": 3521, "san": 40657 } diff --git a/tests/harness/corpus-costs/integer-clone-int32-slots.ts.json b/tests/harness/corpus-costs/integer-clone-int32-slots.ts.json new file mode 100644 index 000000000..91deb4042 --- /dev/null +++ b/tests/harness/corpus-costs/integer-clone-int32-slots.ts.json @@ -0,0 +1 @@ +{ "plain": 3474, "san": 38229 } diff --git a/tests/harness/corpus-costs/localized-failure-cycles.ts.json b/tests/harness/corpus-costs/localized-failure-cycles.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-cycles.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-declarations.ts.json b/tests/harness/corpus-costs/localized-failure-declarations.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-declarations.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-degraded-initializer.ts.json b/tests/harness/corpus-costs/localized-failure-degraded-initializer.ts.json new file mode 100644 index 000000000..3e8d4b6ac --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-degraded-initializer.ts.json @@ -0,0 +1 @@ +{ "plain": 3500 } diff --git a/tests/harness/corpus-costs/localized-failure-expressions.ts.json b/tests/harness/corpus-costs/localized-failure-expressions.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-expressions.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-forward-capture.ts.json b/tests/harness/corpus-costs/localized-failure-forward-capture.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-forward-capture.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-function-slots.ts.json b/tests/harness/corpus-costs/localized-failure-function-slots.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-function-slots.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-held-values.ts.json b/tests/harness/corpus-costs/localized-failure-held-values.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-held-values.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-locals.ts.json b/tests/harness/corpus-costs/localized-failure-locals.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-locals.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-pi-shapes.ts.json b/tests/harness/corpus-costs/localized-failure-pi-shapes.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-pi-shapes.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-push-down.ts.json b/tests/harness/corpus-costs/localized-failure-push-down.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-push-down.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/localized-failure-registered-statics.ts.json b/tests/harness/corpus-costs/localized-failure-registered-statics.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/localized-failure-registered-statics.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/native-worker-terminate-short-inner-loops.ts.json b/tests/harness/corpus-costs/native-worker-terminate-short-inner-loops.ts.json new file mode 100644 index 000000000..c85984a8b --- /dev/null +++ b/tests/harness/corpus-costs/native-worker-terminate-short-inner-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4906, "san": 6969 } diff --git a/tests/harness/corpus-costs/net-unix-socket-connect-default-import.mjs.json b/tests/harness/corpus-costs/net-unix-socket-connect-default-import.mjs.json new file mode 100644 index 000000000..0c62f10ce --- /dev/null +++ b/tests/harness/corpus-costs/net-unix-socket-connect-default-import.mjs.json @@ -0,0 +1 @@ +{ "plain": 3242, "san": 5402 } diff --git a/tests/harness/corpus-costs/net-unix-socket-connect.ts.json b/tests/harness/corpus-costs/net-unix-socket-connect.ts.json new file mode 100644 index 000000000..d03fa6389 --- /dev/null +++ b/tests/harness/corpus-costs/net-unix-socket-connect.ts.json @@ -0,0 +1 @@ +{ "plain": 3555, "san": 6682 } diff --git a/tests/harness/corpus-costs/never-narrowed-literal-argument.ts.json b/tests/harness/corpus-costs/never-narrowed-literal-argument.ts.json new file mode 100644 index 000000000..1af81d8df --- /dev/null +++ b/tests/harness/corpus-costs/never-narrowed-literal-argument.ts.json @@ -0,0 +1 @@ +{ "plain": 2485, "san": 53450 } diff --git a/tests/harness/corpus-costs/non-null-element-computed-index.ts.json b/tests/harness/corpus-costs/non-null-element-computed-index.ts.json new file mode 100644 index 000000000..cd39a7ab4 --- /dev/null +++ b/tests/harness/corpus-costs/non-null-element-computed-index.ts.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 6300 } diff --git a/tests/harness/corpus-costs/record-dynamic-writes.ts.json b/tests/harness/corpus-costs/record-dynamic-writes.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/record-dynamic-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/record-prefix-identity.ts.json b/tests/harness/corpus-costs/record-prefix-identity.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/record-prefix-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/rest-tuple-shorthand-property.ts.json b/tests/harness/corpus-costs/rest-tuple-shorthand-property.ts.json new file mode 100644 index 000000000..0358eaf91 --- /dev/null +++ b/tests/harness/corpus-costs/rest-tuple-shorthand-property.ts.json @@ -0,0 +1 @@ +{ "plain": 2199, "san": 6086 } diff --git a/tests/harness/corpus-costs/short-char-codes.ts.json b/tests/harness/corpus-costs/short-char-codes.ts.json new file mode 100644 index 000000000..621e15852 --- /dev/null +++ b/tests/harness/corpus-costs/short-char-codes.ts.json @@ -0,0 +1 @@ +{ "plain": 4800, "san": 4600 } diff --git a/tests/harness/corpus-costs/speculative-loop-view-headers.ts.json b/tests/harness/corpus-costs/speculative-loop-view-headers.ts.json new file mode 100644 index 000000000..47ead0063 --- /dev/null +++ b/tests/harness/corpus-costs/speculative-loop-view-headers.ts.json @@ -0,0 +1 @@ +{ "plain": 2400 } diff --git a/tests/harness/corpus-costs/string-plus-element-read-return.ts.json b/tests/harness/corpus-costs/string-plus-element-read-return.ts.json new file mode 100644 index 000000000..17a596f9c --- /dev/null +++ b/tests/harness/corpus-costs/string-plus-element-read-return.ts.json @@ -0,0 +1 @@ +{ "plain": 2298, "san": 3533 } diff --git a/tests/harness/corpus-costs/string-spread.ts.json b/tests/harness/corpus-costs/string-spread.ts.json new file mode 100644 index 000000000..d7fbd510a --- /dev/null +++ b/tests/harness/corpus-costs/string-spread.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 4300 } diff --git a/tests/harness/corpus-costs/symbol-keyed-recursive-record.ts.json b/tests/harness/corpus-costs/symbol-keyed-recursive-record.ts.json new file mode 100644 index 000000000..917f53744 --- /dev/null +++ b/tests/harness/corpus-costs/symbol-keyed-recursive-record.ts.json @@ -0,0 +1 @@ +{ "plain": 2702, "san": 43257 } diff --git a/tests/harness/corpus-costs/typed-array-nested-loop-versions.ts.json b/tests/harness/corpus-costs/typed-array-nested-loop-versions.ts.json new file mode 100644 index 000000000..1bee89d1a --- /dev/null +++ b/tests/harness/corpus-costs/typed-array-nested-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 6500, "san": 8800 } diff --git a/tests/harness/corpus-costs/typed-array-offset-indexes.ts.json b/tests/harness/corpus-costs/typed-array-offset-indexes.ts.json new file mode 100644 index 000000000..0b84fb1fe --- /dev/null +++ b/tests/harness/corpus-costs/typed-array-offset-indexes.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2500 } diff --git a/tests/harness/corpus-costs/typeof-ambient-undefined-names.ts.json b/tests/harness/corpus-costs/typeof-ambient-undefined-names.ts.json new file mode 100644 index 000000000..91c82c655 --- /dev/null +++ b/tests/harness/corpus-costs/typeof-ambient-undefined-names.ts.json @@ -0,0 +1 @@ +{ "plain": 4212, "san": 4590 } diff --git a/tests/harness/corpus-costs/undeclared-key-optional-chain/main.cjs.json b/tests/harness/corpus-costs/undeclared-key-optional-chain/main.cjs.json new file mode 100644 index 000000000..8cfabb847 --- /dev/null +++ b/tests/harness/corpus-costs/undeclared-key-optional-chain/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 3000 } diff --git a/tests/harness/corpus-costs/union-record-kept-fields-arm.ts.json b/tests/harness/corpus-costs/union-record-kept-fields-arm.ts.json new file mode 100644 index 000000000..3c750a2dc --- /dev/null +++ b/tests/harness/corpus-costs/union-record-kept-fields-arm.ts.json @@ -0,0 +1 @@ +{ "plain": 4793, "san": 7135 } diff --git a/tests/harness/corpus-costs/union-record-kept-fields-no-discriminant.ts.json b/tests/harness/corpus-costs/union-record-kept-fields-no-discriminant.ts.json new file mode 100644 index 000000000..5e891c40e --- /dev/null +++ b/tests/harness/corpus-costs/union-record-kept-fields-no-discriminant.ts.json @@ -0,0 +1 @@ +{ "plain": 4466, "san": 5253 } diff --git a/tests/harness/corpus-costs/union-tuple-element-read.ts.json b/tests/harness/corpus-costs/union-tuple-element-read.ts.json new file mode 100644 index 000000000..fcdd8c175 --- /dev/null +++ b/tests/harness/corpus-costs/union-tuple-element-read.ts.json @@ -0,0 +1 @@ +{ "plain": 2301, "san": 3138 } diff --git a/tests/harness/corpus-costs/untyped-nullish-scalar-defaults.mjs.json b/tests/harness/corpus-costs/untyped-nullish-scalar-defaults.mjs.json new file mode 100644 index 000000000..e822c3bda --- /dev/null +++ b/tests/harness/corpus-costs/untyped-nullish-scalar-defaults.mjs.json @@ -0,0 +1 @@ +{ "plain": 3711, "san": 5463 } diff --git a/tests/harness/corpus-costs/var-object-filled-later.js.json b/tests/harness/corpus-costs/var-object-filled-later.js.json new file mode 100644 index 000000000..1e74b61e7 --- /dev/null +++ b/tests/harness/corpus-costs/var-object-filled-later.js.json @@ -0,0 +1 @@ +{ "plain": 3400 } diff --git a/tests/harness/corpus-costs/while-entry-range-versions.ts.json b/tests/harness/corpus-costs/while-entry-range-versions.ts.json new file mode 100644 index 000000000..2a685047e --- /dev/null +++ b/tests/harness/corpus-costs/while-entry-range-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3000 } diff --git a/tests/harness/corpus-costs/width-layout-identity.ts.json b/tests/harness/corpus-costs/width-layout-identity.ts.json new file mode 100644 index 000000000..4f6ccc65c --- /dev/null +++ b/tests/harness/corpus-costs/width-layout-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 4655, "san": 33380 } diff --git a/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json b/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json new file mode 100644 index 000000000..de667a408 --- /dev/null +++ b/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 2940, "san": 38518 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index fde16a901..82e541140 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -428,6 +428,93 @@ test("--explain-dynamic attributes each refusal to the unit the dynamic tier wou ); }); +test("--explain-dynamic lists the type failures localized failure would turn into dynamic slots", async () => { + const { coverage } = analyze(fixture("localized-failure.ts"), { explainDynamic: true }); + const report = coverage.tiers!.localizedFailure; + // Locals, fields and parameters degrade (stages L1 and L2), and the + // 'any' field compiles; only the refused WeakRef field and its + // construction remain. + expect(coverage.dynamicSlots?.map((s) => [s.slot, s.name])).toEqual([ + ["field", "tools"], + ["param", "progress"], + ["local", "callbacks"], + ]); + expect(report.errors).toBe(2); + expect(report.typeFailures).toEqual({ degradable: 0, refused: 1 }); + expect(report.slots).toEqual([]); + expect(report.cascades).toBe(1); + expect(report.remainingByCode).toEqual({ SC1090: 1, SC2020: 1 }); + const relative = JSON.parse( + JSON.stringify(coverage.tiers).replaceAll(repoRoot + "/", ""), + ) as typeof coverage.tiers; + await expect(renderExplainDynamic(relative!)).toMatchFileSnapshot( + "__snapshots__/coverage-explain-localized-failure.txt", + ); +}); + +test("a local or module global whose declared type does not compile becomes a dynamic slot", async () => { + const program = join(repoRoot, "tests/corpus/localized-failure-locals.ts"); + const { coverage, sourceTexts } = analyze(program); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.dynamicSlots?.map((s) => [s.slot, s.name, s.declaredType, s.code])).toEqual([ + ["global", "current", "Callbacks | undefined", "SC2009"], + ["global", "all", "(number | Callbacks)[]", "SC2009"], + ["local", "local", "Callbacks | undefined", "SC2009"], + ]); + // The loop over `all` is top-level code: its dynamic operations count too. + expect(coverage.dynamicSlots?.find((s) => s.name === "all")?.operations).toBeGreaterThan(0); + expect(coverage.performance?.filter((d) => d.code === "SC6104")).toHaveLength(3); + expect(coveragePasses(coverage, "blockers")).toBe(true); + expect(coveragePasses(coverage, "dynamic")).toBe(true); + expect(coveragePasses(coverage, "dynamic-slots")).toBe(false); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.dynamicSlots?.map((s) => [s.kind, s.name, s.line])).toEqual([ + ["global", "current", 10], + ["global", "all", 36], + ["local", "local", 14], + ]); + expect(envelope.diagnostics.filter((d) => d.code === "SC6104").map((d) => d.severity)).toEqual([ + "warning", + "warning", + "warning", + ]); + const text = renderCoverage(coverage, { sourceTexts, root: repoRoot }); + expect(text).toContain("dynamic slots 3"); + expect(text).toContain("module global 'current' (declared 'Callbacks | undefined')"); + // --tier=static-only keeps every refusal. + const strict = analyze(program, { tier: "static-only" }).coverage; + expect(strict.dynamicSlots).toBeUndefined(); + expect(strict.diagnostics.map((d) => d.code)).toContain("SC2009"); +}); + +test("dynamic slots rank by their dynamic operations and name themselves in SC6101", () => { + const { coverage } = analyze(fixture("dynamic-slot-hot.ts")); + const slots = coverage.dynamicSlots ?? []; + expect(slots.find((s) => s.name === "plugins")?.partial).toBe(true); + expect(slots.some((s) => (s.operations ?? 0) > 0)).toBe(true); + const hot = coverage.performance?.find((d) => d.code === "SC6101"); + expect(hot?.message).toContain("`hook` is a dynamic piece of `Plugin[]` (declared `Hook`"); +}); + +test("a type that is a dynamic piece and a slot of its own counts toward its own slot", () => { + const { coverage } = analyze(fixture("dynamic-slot-shared.ts")); + const slots = coverage.dynamicSlots ?? []; + expect(slots.find((s) => s.name === "current")?.operations).toBeGreaterThan(0); + expect(slots.find((s) => s.name === "plugins")?.operations ?? 0).toBe(0); + const hot = coverage.performance?.find((d) => d.code === "SC6101"); + expect(hot?.message).toContain("`current` is a dynamic slot (declared `Hook`"); +}); + +test("dynamic slots of one declared type count their own operations", () => { + const { coverage } = analyze(fixture("dynamic-slot-same-type.ts")); + const slots = coverage.dynamicSlots ?? []; + const hot = slots.find((s) => s.name === "hot")?.operations ?? 0; + const cold = slots.find((s) => s.name === "cold")?.operations ?? 0; + expect(hot).toBeGreaterThan(cold); + const hotWarning = coverage.performance?.find((d) => d.code === "SC6101"); + expect(hotWarning?.message).toContain("`hot` is a dynamic slot (declared `Hook`"); +}); + test("CLI: --tier and --explain-dynamic", () => { const scriptcCli = join(repoRoot, "packages/cli/src/main.ts"); const outDir = mkdtempSync(join(tmpdir(), "scriptc-tier-")); @@ -530,13 +617,18 @@ test("runtime-optional refined unions report blockers without crashing", () => { }); test("a rejected class reached only through a receiver probe reports its own diagnostic once", () => { - const { coverage } = analyze(join(repoRoot, "tests/diagnostics/deferred-class-receiver/main.ts")); + const program = join(repoRoot, "tests/diagnostics/deferred-class-receiver/main.ts"); + // The class's refusal, as the diagnostics corpus pins it (--tier=static-only). + const { coverage } = analyze(program, { tier: "static-only" }); const messages = coverage.diagnostics.map((d) => d.message); expect( messages.filter((m) => m.startsWith("'MapIterator'-typed class fields")), ).toHaveLength(1); expect(messages.some((m) => m.startsWith("constructing through a class value"))).toBe(true); expect(coverage.unreached?.diagnostics ?? []).toEqual([]); + // Localized failure retries the class where the probe's statement commits + // the flush, with the field policy lifted, and it compiles. + expect(analyze(program).coverage.diagnostics).toEqual([]); }); test("settled generic rest-order fences count each source statement once", () => { diff --git a/tests/harness/diagnostics.test.ts b/tests/harness/diagnostics.test.ts index 77fd5937d..895ed9647 100644 --- a/tests/harness/diagnostics.test.ts +++ b/tests/harness/diagnostics.test.ts @@ -39,7 +39,14 @@ describe(`diagnostics corpus (${files.length} programs)`, () => { if (name.startsWith("checker-panic-")) process.env["SCRIPTC_CHECKER"] = "tsgo"; let result: Awaited>; try { - result = await compile(file, { outPath: join(outDir, "never"), outDir }); + // --tier=static-only: these programs pin today's refusals, which + // localized failure otherwise turns into checked dynamic slots + // (the snapshots double as its proof that static-only keeps them). + result = await compile(file, { + outPath: join(outDir, "never"), + outDir, + tier: "static-only", + }); } finally { if (checker === undefined) delete process.env["SCRIPTC_CHECKER"]; else process.env["SCRIPTC_CHECKER"] = checker; diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index dfd037570..97def0cc6 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -169,6 +169,33 @@ describe(`dynamic-boundary checks (scriptc-only${sanitize ? ", sanitized" : ""}) ).toBe(""); }); + test("a boxed native value cast to another layout is copied (decision D3)", async () => { + // Node's `as` is a no-op, so these writes would show through every + // holder. scriptc copies a boxed native value into a cast's different + // layout for now (the object model's decision D3: copy, throw, or + // share storage); the copy keeps one identity per boxed value. + const r = await compileAndRun( + "native-layout-casts", + `type Point = { x: number; y: number }; +const dict: Record = { x: 1, y: 2 }; +const v: unknown = dict; +const p = v as Point; +p.x = 5; +console.log(dict.x, p.x, (v as Point) === p); +const nums: number[] = [1, 2]; +const u: unknown = nums; +console.log((u as number[]) === nums); +const wide = u as (number | string)[]; +wide.push("s"); +console.log(nums.length, wide.length); +console.log(JSON.stringify({ ...(v as Record) })); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe(["1 5 true", "true", "2 3", '{"x":1,"y":2}', ""].join("\n")); + expect(r.stderr).toBe(""); + }); + test("wrong-typed field throws with the path", async () => { const r = await compileAndRun( "wrong-type", diff --git a/tests/harness/heap-census.test.ts b/tests/harness/heap-census.test.ts new file mode 100644 index 000000000..b8ca2b799 --- /dev/null +++ b/tests/harness/heap-census.test.ts @@ -0,0 +1,113 @@ +/* The heap census (backend/llvm/heap-census.ts): a program built with + * SCRIPTC_HEAP_CENSUS counts class instances and the stores, lengths and + * accesses of array-typed fields, and writes them at exit, on a normal exit + * and on process.exit alike. */ +import { execFile } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } 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 { compile } from "@scriptc/compiler"; + +const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const PROGRAM = ` +class Node { + kids: Node[]; + tags: string[] | undefined; + constructor(kids: Node[]) { + this.kids = kids; + if (kids.length > 1) this.tags = ["a", "b"]; + else this.tags = undefined; + } +} +let total = 0; +const roots: Node[] = []; +for (let i = 0; i < 100; i++) { + const leaves: Node[] = []; + for (let j = 0; j < i % 6; j++) leaves.push(new Node([])); + roots.push(new Node(leaves)); +} +for (const r of roots) total += r.kids.length + (r.tags?.length ?? 0); +console.log(total); +`; + +async function census(source: string): Promise<{ stdout: string; counts: Map }> { + const dir = await mkdtemp(join(tmpdir(), "scriptc-heap-census-")); + const saved = process.env["SCRIPTC_HEAP_CENSUS"]; + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + process.env["SCRIPTC_HEAP_CENSUS"] = "1"; + const result = await compile(entry, { + backend: "llvm", + sanitize, + outDir: dir, + outPath: join(dir, "program"), + }); + if (saved === undefined) delete process.env["SCRIPTC_HEAP_CENSUS"]; + else process.env["SCRIPTC_HEAP_CENSUS"] = saved; + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) throw new Error("compile failed"); + const out = join(dir, "census.tsv"); + const { stdout } = await exec(result.binaryPath, [], { + env: { ...process.env, SCRIPTC_HEAP_CENSUS: out }, + timeout: 120_000, + }); + const counts = new Map(); + for (const line of (await readFile(out, "utf8")).trimEnd().split("\n")) { + const [kind, cls, field, count] = line.split("\t"); + counts.set(`${kind} ${cls}${field ? "." + field : ""}`, Number(count)); + } + return { stdout, counts }; + } finally { + if (saved === undefined) delete process.env["SCRIPTC_HEAP_CENSUS"]; + else process.env["SCRIPTC_HEAP_CENSUS"] = saved; + await rm(dir, { recursive: true, force: true }); + } +} + +const EXPECTED: [string, number][] = [ + // 100 roots and 246 leaves (i % 6 children each). + ["new Node", 346], + ["store Node.kids", 346], + ["elements Node.kids", 246], + ["len0 Node.kids", 263], + ["len1 Node.kids", 17], + ["len2 Node.kids", 17], + ["len3-4 Node.kids", 33], + ["len5-8 Node.kids", 16], + ["len9-16 Node.kids", 0], + // Literals count where the store itself writes one. + ["literal Node.kids", 0], + ["store Node.tags", 346], + ["literal Node.tags", 66], + // An undefined tags field counts as length 0. + ["len0 Node.tags", 280], + ["len2 Node.tags", 66], + ["elements Node.tags", 132], +]; + +test.skipIf(process.platform !== "linux")( + "the census counts instances and array field stores at a normal exit", + async () => { + const { stdout, counts } = await census(PROGRAM); + expect(stdout).toBe("378\n"); + for (const [key, count] of EXPECTED) expect([key, counts.get(key)]).toEqual([key, count]); + // Every store also computes the field's address. + expect(counts.get("access Node.kids")).toBeGreaterThanOrEqual(346 + 100); + }, + 240_000, +); + +test.skipIf(process.platform !== "linux")( + "the census is written on process.exit too", + async () => { + const { stdout, counts } = await census(PROGRAM + "process.exit(0);\n"); + expect(stdout).toBe("378\n"); + expect(counts.get("new Node")).toBe(346); + }, + 240_000, +); diff --git a/tests/harness/ir-fields-generator.test.ts b/tests/harness/ir-fields-generator.test.ts new file mode 100644 index 000000000..f3cba57a1 --- /dev/null +++ b/tests/harness/ir-fields-generator.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import ts from "typescript"; +import { expect, test } from "vitest"; +import { generateIrFields } from "../../scripts/generate-ir-fields.mjs"; + +const compilerDir = join(import.meta.dirname, "../../packages/compiler"); +const irPath = join(compilerDir, "src/ir/ir.ts"); +const generatedPath = join(compilerDir, "src/ir/fields.generated.ts"); +const irText = readFileSync(irPath, "utf8"); +const arrayGet = '{ kind: "arrayGet"; arr: IrExpr; index: IrExpr; type: IrType; loc: SrcLoc }'; + +/** ir.ts with `field` added to the arrayGet node. */ +function withArrayGetField(field: string): string { + expect(irText).toContain(arrayGet); + return irText.replace(arrayGet, arrayGet.replace("loc: SrcLoc }", `loc: SrcLoc; ${field} }`)); +} + +/** The type errors of `generated` as fields.generated.ts, with `ir` as ir.ts, + * under the compiler package's options. */ +function generatedErrors(ir: string, generated: string): string[] { + const parsed = ts.getParsedCommandLineOfConfigFile( + join(compilerDir, "tsconfig.json"), + { noEmit: true, incremental: false }, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, + )!; + const host = ts.createCompilerHost(parsed.options); + const readSourceFile = host.getSourceFile; + host.getSourceFile = (file, languageVersion, ...rest) => + file === irPath + ? ts.createSourceFile(file, ir, languageVersion) + : file === generatedPath + ? ts.createSourceFile(file, generated, languageVersion) + : readSourceFile.call(host, file, languageVersion, ...rest); + const program = ts.createProgram([generatedPath], parsed.options, host); + return ts + .getPreEmitDiagnostics(program, program.getSourceFile(generatedPath)) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n")); +} + +test("a nested record of scalar fields passes the checks once the readers are regenerated", () => { + const ir = withArrayGetField("counts?: { hits: number; misses: number | undefined }"); + expect(generatedErrors(ir, generateIrFields(ir))).toEqual([]); +}); + +test("the record's own fields are checked: an added field or a widened scalar fails with its name", () => { + const generated = generateIrFields( + withArrayGetField("counts?: { hits: number; misses: number | undefined }"), + ); + const added = generatedErrors( + withArrayGetField("counts?: { hits: number; misses: number | undefined; label: string }"), + generated, + ); + expect(added).toEqual([`Type '"label"' does not satisfy the constraint 'never'.`]); + const widened = generatedErrors( + withArrayGetField("counts?: { hits: number | string; misses: number | undefined }"), + generated, + ); + expect(widened).toEqual([`Type 'string' does not satisfy the constraint 'never'.`]); +}); diff --git a/tests/harness/native-cli-packaging.test.ts b/tests/harness/native-cli-packaging.test.ts index e69f9d9fe..55dc07a4e 100644 --- a/tests/harness/native-cli-packaging.test.ts +++ b/tests/harness/native-cli-packaging.test.ts @@ -1,6 +1,14 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, expect, test } from "vitest"; @@ -71,6 +79,10 @@ function fixture(windows = true) { ), "fixture", ); + write( + join(distribution, "lib/llvm/package.json"), + JSON.stringify({ name: `@scriptc/llvm-${platform}`, version: identity.version }), + ); mkdirSync(join(distribution, "lib/runtime-sources/vendor"), { recursive: true }); mkdirSync(join(distribution, "lib/declarations"), { recursive: true }); for (const path of [join(distribution, "lib/runtime"), join(packages, `runtime-${platform}`)]) { @@ -133,6 +145,80 @@ test.each([true, false])( }, ); +test.each([true, false])( + "a platform package depends on its helper and runtime pack, and its archive bundles them (Windows=%s)", + (windows) => { + const f = fixture(windows); + const helper = `@scriptc/llvm-${f.platform}`; + const runtime = `@scriptc/runtime-${f.platform}`; + const manifest: Record = { ...f.manifest }; + delete manifest.llvm_package; + delete manifest.runtime_pack; + delete manifest.runtime_packs; + writeFileSync(f.manifestPath, JSON.stringify(manifest)); + const notices = join(f.cli, "dist/THIRD_PARTY_NOTICES"); + writeFileSync( + notices, + `LLVM licensing and notices: LICENSE and THIRD_PARTY_NOTICES in the ${helper} package.\n`, + ); + rmSync(join(f.cli, "dist/lib/llvm"), { recursive: true }); + rmSync(join(f.cli, "dist/lib/runtime"), { recursive: true }); + const helperPackage = join(f.packages, `llvm-${f.platform}`); + const codegen = windows ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen"; + mkdirSync(join(helperPackage, "bin"), { recursive: true }); + for (const [name, directory] of [ + [helper, helperPackage], + [runtime, join(f.packages, `runtime-${f.platform}`)], + ] as const) { + writeFileSync(join(directory, "package.json"), JSON.stringify({ name, version: "1.2.3" })); + // pnpm links workspace dependencies into the package's node_modules. + mkdirSync(join(f.cli, "node_modules/@scriptc"), { recursive: true }); + symlinkSync(directory, join(f.cli, "node_modules", name), "junction"); + } + writeFileSync(join(helperPackage, "bin", codegen), "helper"); + const identity = { name: `@scriptc/cli-${f.platform}`, version: "1.2.3" }; + writeFileSync(join(f.cli, "package.json"), JSON.stringify(identity)); + const missing = run("verify-native-cli.mjs", f.cli); + expect(missing.status).toBe(1); + expect(missing.stderr).toContain(`must depend on its @scriptc/llvm-* package at 1.2.3`); + writeFileSync( + join(f.cli, "package.json"), + JSON.stringify({ + ...identity, + dependencies: { [helper]: "1.2.3", [runtime]: "workspace:*" }, + }), + ); + const verified = run("verify-native-cli.mjs", f.cli); + expect(verified.status, verified.stderr).toBe(0); + const output = join(f.directory, "archives"); + const packed = run("package-native-cli.mjs", f.packages, output); + expect(packed.status, packed.stderr).toBe(0); + const unpacked = join(f.directory, "unpacked"); + mkdirSync(unpacked); + const archive = join(output, `scriptc-1.2.3-${f.platform}.tar.gz`); + const extracted = spawnSync("tar", ["-xzf", archive, "-C", unpacked], { encoding: "utf8" }); + expect(extracted.status, extracted.stderr).toBe(0); + const bundled = JSON.parse(readFileSync(join(unpacked, "bin", f.executable + ".json"), "utf8")); + const slash = windows ? "\\" : "/"; + expect(bundled.llvm_package).toBe(["..", "lib", "llvm"].join(slash)); + expect(bundled.runtime_pack).toBe(["..", "lib", `runtime-${f.platform}`].join(slash)); + expect(bundled.runtime_packs).toEqual([ + { target: f.manifest.target, path: bundled.runtime_pack }, + ]); + expect(readFileSync(join(unpacked, "lib/llvm/bin", codegen), "utf8")).toBe("helper"); + expect( + JSON.parse( + readFileSync(join(unpacked, `lib/runtime-${f.platform}/runtime-pack.json`), "utf8"), + ).target.name, + ).toBe(f.manifest.target); + expect(readFileSync(join(unpacked, "THIRD_PARTY_NOTICES"), "utf8")).toBe( + "LLVM licensing and notices: lib/llvm/LICENSE and lib/llvm/THIRD_PARTY_NOTICES.\n", + ); + // The npm package itself is unchanged. + expect(JSON.parse(readFileSync(f.manifestPath, "utf8"))).toEqual(manifest); + }, +); + test.each([ "C:\\outside\\tsc.exe", "C:tsc.exe", diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 8e133f6e2..9623b7563 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -50,7 +50,6 @@ const KNOWN: Record = { "esbuild-require": { build: "SC2013" }, "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, - "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC1101" }, "import-refusal": { build: "SC2020" }, "island-registry": { build: "SC2011" }, @@ -58,9 +57,9 @@ const KNOWN: Record = { divergence: "Timeout handles are numbers (typeof) and lose their ref flag once fired or cleared", }, - "island-web-plumbing": { run: "SC2020" }, + "island-web-plumbing": { run: "SC1090" }, "lazy-traps": { build: "SC2013" }, - "misc-shims": { run: "SC2020" }, + "misc-shims": { run: "SC1090" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, "node24-commonjs-metadata": { run: "SC2020" }, diff --git a/tests/harness/runtime-tree-shaking.test.ts b/tests/harness/runtime-tree-shaking.test.ts index b33a0d2f9..92584a59e 100644 --- a/tests/harness/runtime-tree-shaking.test.ts +++ b/tests/harness/runtime-tree-shaking.test.ts @@ -142,6 +142,9 @@ nativeToolchainTest( "scr_url_", "scr_json_parse", "scr_date_", + // The generator runtime installs its tag reader with its first + // generator; a load-time constructor would retain it everywhere. + "scr_gen_tag", ]) { expect(helloSymbols, `hello retains ${family}`).not.toContain(family); } @@ -159,8 +162,10 @@ nativeToolchainTest( // Symbol absence is the primary reachability contract. Keep a deliberately // roomy, platform-specific hello-world ceiling too: it catches losing - // section GC without pinning an exact linker/SDK byte count. The canonical - // Linux build is about 84KB and current Mach-O builds are about 70KB. Most + // section GC without pinning an exact linker/SDK byte count. The Linux + // build is about 65KB with lld (CI's pinned LLVM provides it) and about + // 94KB with GNU ld, which keeps a unit's string literals whole once any of + // them is reachable; current Mach-O builds are about 70KB. Most // of Linux hello's runtime code is the cycle collector, which its exit // path reaches (scr_lib_cleanup releases an array). ELF segments are page // aligned, so the Linux size moves in 4 KiB steps whenever .text crosses diff --git a/tests/harness/sandbox-config.test.ts b/tests/harness/sandbox-config.test.ts index cedaef600..532fb5c4e 100644 --- a/tests/harness/sandbox-config.test.ts +++ b/tests/harness/sandbox-config.test.ts @@ -5,6 +5,7 @@ import { sandboxBootstrapCommand, sandboxEffectWorkerAllocation, sandboxImageConfig, + sandboxNativeReference, sandboxRunnerConfig, sandboxTestSourceConfig, sandboxTestWorkerAllocation, @@ -97,6 +98,10 @@ test("the managed-image bootstrap pins the custom image's Node, pnpm, and LLVM", expect(bootstrap).toContain("libclang-rt-22-dev"); expect(bootstrap).toContain("llvm-22-dev"); expect(bootstrap).toContain("ninja-build"); + // CI links with ld.lld; without it Sandbox binaries link with GNU ld. + expect(bootstrap).toContain("lld-22"); + const dockerfile = readFileSync(new URL("../../Dockerfile.sandbox", import.meta.url), "utf8"); + expect(dockerfile).toContain("lld-22"); }); test("a custom image does not select the Sandbox team or project", () => { @@ -154,6 +159,24 @@ test("ambiguous or malformed snapshot sources fail before creating workers", () ); }); +test("a native reference checkout needs a prepared source and an absolute path", () => { + const snapshot = { SCRIPTC_SANDBOX_SNAPSHOT: "snap_ABC123" }; + expect(sandboxNativeReference(snapshot)).toBeUndefined(); + expect( + sandboxNativeReference({ + ...snapshot, + SCRIPTC_SANDBOX_NATIVE_REFERENCE: " /vercel/sandbox/prev/ ", + }), + ).toBe("/vercel/sandbox/prev"); + expect(() => + sandboxNativeReference({ SCRIPTC_SANDBOX_NATIVE_REFERENCE: "/vercel/sandbox/prev" }), + ).toThrow("needs SCRIPTC_SANDBOX_SNAPSHOT"); + for (const path of ["prev", "/a/../b", "/a b", "/a;rm"]) + expect(() => + sandboxNativeReference({ ...snapshot, SCRIPTC_SANDBOX_NATIVE_REFERENCE: path }), + ).toThrow("absolute path"); +}); + test("OIDC authentication wins and supplies its own Sandbox scope", () => { const config = sandboxVercelConfig({ VERCEL_OIDC_TOKEN: "header.payload.signature", diff --git a/tests/harness/self-hosting-emission.test.ts b/tests/harness/self-hosting-emission.test.ts index 3977ede29..e6364b855 100644 --- a/tests/harness/self-hosting-emission.test.ts +++ b/tests/harness/self-hosting-emission.test.ts @@ -59,7 +59,9 @@ test("the production layout and metadata stage lowers statically", async () => { }); for (const backend of ["llvm"] as const) { - test(`native LLVM layout and metadata emission (${backend})`, async () => { + // The sanitized dev build of the stage takes 170 to 300 s of CI time beside + // its shard's other tests. + test(`native LLVM layout and metadata emission (${backend})`, { timeout: 600_000 }, async () => { const dir = mkdtempSync( join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-emission-"), ); diff --git a/tests/harness/self-hosting-frontend-lowering.test.ts b/tests/harness/self-hosting-frontend-lowering.test.ts index e7223c0cc..a544268f5 100644 --- a/tests/harness/self-hosting-frontend-lowering.test.ts +++ b/tests/harness/self-hosting-frontend-lowering.test.ts @@ -18,6 +18,9 @@ test("the complete production frontend lowers with zero rejected statements", as const { stdout, stderr } = await promisify(execFile)( process.execPath, [ + // The lowering holds about 3 GB of heap; Node's default limit is lower + // on hosts with less memory. + "--max-old-space-size=8192", "--import", "tsx", "--input-type=module", diff --git a/tests/harness/self-hosting-native-driver.test.ts b/tests/harness/self-hosting-native-driver.test.ts index 1cc920bd2..60969d0b2 100644 --- a/tests/harness/self-hosting-native-driver.test.ts +++ b/tests/harness/self-hosting-native-driver.test.ts @@ -1,4 +1,4 @@ -import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs"; +import { seedBuildEnvironment, verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs"; import { execFile, spawnSync } from "node:child_process"; import { chmodSync, @@ -26,6 +26,7 @@ import { } from "../../scripts/ci-native-bootstrap.mjs"; import { withoutFirstLine } from "./large-artifacts.js"; import { bootstrapStep } from "./self-hosting-timing.js"; +import { unbundlePlatformPackages } from "../../scripts/native-cli-bundle.mjs"; const root = join(import.meta.dirname, "../.."); const exec = promisify(execFile); @@ -89,12 +90,7 @@ test( // seed avoids optimizing the huge compiler module before ASan can // exercise its complete traversal, serialization and LLVM emission. ...options, - env: { - ...process.env, - SCRIPTC_SAN: sanitize ? "1" : "", - SCRIPTC_NATIVE_EMIT_IR: "1", - SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release", - }, + env: { ...process.env, ...seedBuildEnvironment(sanitize) }, }, ), ); @@ -110,6 +106,21 @@ test( join(packageDirectory, "package.json"), ); cpSync(distribution, join(packageDirectory, "dist"), { recursive: true }); + // The npm package depends on its LLVM helper and runtime pack. Link + // them into its node_modules, as pnpm links a workspace package's + // dependencies, so the package checks resolve them. + unbundlePlatformPackages( + join(packageDirectory, "dist"), + join(packageDirectory, "dist/bin/scriptc.json"), + "@scriptc/llvm-linux-x64-gnu", + ); + mkdirSync(join(packageDirectory, "node_modules/@scriptc"), { recursive: true }); + for (const name of ["llvm-linux-x64-gnu", "runtime-linux-x64-gnu"]) + symlinkSync( + join(root, "packages", name), + join(packageDirectory, "node_modules/@scriptc", name), + "dir", + ); writeFileSync(packageDirectory + ".ready", "ready\n"); } // All compiler assets must survive moving the complete distribution. diff --git a/tests/harness/shape-overlay.test.ts b/tests/harness/shape-overlay.test.ts new file mode 100644 index 000000000..6c843c7cb --- /dev/null +++ b/tests/harness/shape-overlay.test.ts @@ -0,0 +1,126 @@ +/* The shape overlay (shape-inference/overlay.ts): with SCRIPTC_SHAPE_OVERLAY=1 + * the zod3-boundary npm case compiles zod's JavaScript with analysis-proven JSDoc + * types, reports what it typed and checks (SC6103), and prints what Node + * prints; the shapeOverlay: false option (the CLI's --no-shape-overlay) + * turns it off. */ +import { execFile } from "node:child_process"; +import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/main.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/shape-overlay"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +async function withOverlay(run: () => Promise): Promise { + const saved = process.env["SCRIPTC_SHAPE_OVERLAY"]; + process.env["SCRIPTC_SHAPE_OVERLAY"] = "1"; + try { + return await run(); + } finally { + if (saved === undefined) delete process.env["SCRIPTC_SHAPE_OVERLAY"]; + else process.env["SCRIPTC_SHAPE_OVERLAY"] = saved; + } +} + +test( + "the overlay types zod's JavaScript and keeps Node's output", + { timeout: 900_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(entry, { outPath: join(outDir, "on"), outDir, sanitize }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const report = (result.warnings ?? []).find((w) => w.code === "SC6103"); + expect(report?.message).toMatch( + /shape inference typed \d+ declarations in \d+ files? of 1 package \(zod\)/, + ); + const run = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + run.child.stdin?.end(); + const node = await execFileAsync(process.execPath, [entry], { + cwd: dirname(entry), + encoding: "utf8", + }); + expect((await run).stdout).toBe(node.stdout); + }, +); + +test("shapeOverlay: false compiles without the overlay", { timeout: 900_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(entry, { outPath: join(outDir, "off"), outDir, sanitize, shapeOverlay: false }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect((result.warnings ?? []).some((w) => w.code === "SC6103")).toBe(false); +}); + +// shape-facts: results that include the undefined of a fall-through or a bare +// return keep the caller's falsy branch, comment-like text in literals keeps +// its value, and a replaced branch keeps its var declared, also when two +// replaced branches declare the same var. +const factsEntry = join(repoRoot, "tests/fixtures/npm/cases/shape-overlay-facts/main.ts"); + +test( + "the overlay keeps undefined results, literal text and hoisted vars", + { timeout: 900_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await withOverlay(() => + compile(factsEntry, { outPath: join(outDir, "facts"), outDir, sanitize }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const report = (result.warnings ?? []).find((w) => w.code === "SC6103"); + expect(report?.message).toMatch(/of 1 package \(shape-facts\)/); + expect(report?.message).toMatch(/replaces 3 branches it proved unreachable/); + const native = await execFileAsync(result.binaryPath, [], { + cwd: dirname(factsEntry), + encoding: "utf8", + }); + const node = await execFileAsync(process.execPath, [factsEntry], { + cwd: dirname(factsEntry), + encoding: "utf8", + }); + expect(native.stdout).toBe(node.stdout); + }, +); + +// `"scriptc": { "packages": { "": { "shapes": false } } }` in the +// project's package.json compiles that package without the overlay. +test( + "a package with shapes: false compiles without the overlay", + { timeout: 900_000 }, + async () => { + const project = join(outDir, "facts-shapeless"); + rmSync(project, { recursive: true, force: true }); + cpSync(dirname(factsEntry), project, { recursive: true }); + writeFileSync( + join(project, "package.json"), + JSON.stringify({ + name: "shape-overlay-facts-case", + private: true, + type: "module", + scriptc: { packages: { "shape-facts": { shapes: false } } }, + }), + ); + const result = await withOverlay(() => + compile(join(project, "main.ts"), { + outPath: join(outDir, "facts-shapeless-bin"), + outDir, + sanitize, + }), + ); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect((result.warnings ?? []).some((w) => w.code === "SC6103" || w.code === "SC8004")).toBe( + false, + ); + }, +); diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 66c2934f2..68d4b1807 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -316,6 +316,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { // Weak-collection hashes mix 32-bit pointers in 64 bits. "4591-weak-keys-union-records.ts", "7742-weak-collections-many-keys.ts", + // zlib's portable build (CPU_NO_SIMD) compresses to Node's bytes. + "8405-zlib-compressed-bytes.ts", ])("the LLVM target runs %s byte-identically to Node", async (fixture) => { const entry = join(repoRoot, "tests/corpus", fixture); const outDir = await mkdtemp("/tmp/scriptc-wasi-"); diff --git a/tests/harness/zero-cost-gate.test.ts b/tests/harness/zero-cost-gate.test.ts index 565f8ba71..e99eca8d1 100644 --- a/tests/harness/zero-cost-gate.test.ts +++ b/tests/harness/zero-cost-gate.test.ts @@ -1,14 +1,20 @@ import { spawnSync } from "node:child_process"; import { expect, test } from "vitest"; import { + cachedComparison, cachegrindCounts, compareArtifact, compareCounts, compareExecutables, definedFunctionNames, dynamicTierSymbols, + mergeReports, nmOutput, + programInputsHash, + programLines, programVerdict, + recordedBaseline, + timingVerdict, } from "../../scripts/zero-cost-compare.mjs"; const ir = (functions: object[], globals: object[]) => @@ -191,3 +197,105 @@ test("the zero-cost gate fails differing programs unless they match the signatur expect(verdict({}, false, failed, failed, true)).toEqual({ verdict: "both-fail", fails: true }); expect(verdict({}, true, ok, failed)).toEqual({ verdict: "build-error", fails: true }); }); + +test("a recorded baseline settles identical programs and shared build errors", () => { + const hashes = { ir: "a", llvm: "b", exe: "c" }; + const entry = { ok: true, ms: 12, hashes, exeBytes: 100 }; + expect(cachedComparison(entry, { ok: true }, { ...hashes })).toEqual({ + baseline: { ok: true, ms: 12 }, + artifacts: { + ir: { identical: true }, + llvm: { identical: true }, + exe: { identical: true, bytes: { baseline: 100, candidate: 100 } }, + }, + }); + // Any other hash, a missing artifact or no record builds the baseline. + expect(cachedComparison(entry, { ok: true }, { ...hashes, exe: "d" })).toBeNull(); + expect( + cachedComparison({ ...entry, hashes: { ...hashes, ir: null } }, { ok: true }, hashes), + ).toBeNull(); + expect(cachedComparison(undefined, { ok: true }, hashes)).toBeNull(); + expect(cachedComparison(entry, { ok: false, error: "SC1090" }, {})).toBeNull(); + const failed = { ok: false, ms: 3, error: "SC1090" }; + expect(cachedComparison(failed, { ok: false, error: "SC1090" }, {})).toEqual({ + baseline: { ok: false, error: "SC1090", ms: 3, cached: true }, + artifacts: {}, + }); + expect(cachedComparison(failed, { ok: false, error: "SC1091" }, {})).toBeNull(); + expect(cachedComparison(failed, { ok: true }, hashes)).toBeNull(); +}); + +test("a recorded baseline applies only to the program inputs it was recorded for", () => { + const files: Record = { + "/p/tests/corpus/a/main.ts": 'import { f } from "./lib.ts";', + "/p/tests/corpus/a/lib.ts": "export const f = 1;", + }; + const read = (file: string) => Buffer.from(files[file]!); + const paths = Object.keys(files); + const inputs = programInputsHash("/p", paths, "release", read); + expect(programInputsHash("/p", [...paths].reverse(), "release", read)).toBe(inputs); + expect(programInputsHash("/p", paths, "speed", read)).not.toBe(inputs); + expect(programInputsHash("/q", paths, "release", read)).not.toBe(inputs); + // An edit to an import changes the hash as an edit to the entry does. + files["/p/tests/corpus/a/lib.ts"] = "export const f = 2;"; + const edited = programInputsHash("/p", paths, "release", read); + expect(edited).not.toBe(inputs); + const entry = { inputs, ok: false, ms: 3, error: "SC1090" }; + const cache = { programs: { "corpus/a/main.ts": entry } }; + expect(recordedBaseline(cache, "corpus/a/main.ts", inputs)).toBe(entry); + // A record of the old sources must not settle the new sources as both-fail or identical. + expect(recordedBaseline(cache, "corpus/a/main.ts", edited)).toBeUndefined(); + expect(recordedBaseline(cache, "corpus/b/main.ts", inputs)).toBeUndefined(); + expect(recordedBaseline(null, "corpus/a/main.ts", inputs)).toBeUndefined(); +}); + +test("merged gate shards judge compile time and comparisons over every shard", () => { + const trees = { baseline: { revision: "a" }, candidate: { revision: "b" } }; + const row = (id: string, ratio: number) => ({ + id, + baselineMs: [100, 100, 100], + candidateMs: [100 * ratio, 100 * ratio, 100 * ratio], + ratios: [ratio, ratio, ratio], + }); + const shard = ( + programs: object[], + rows: object[], + failures: string[] = [], + globals: string[] = [], + ) => ({ + schema: 1, + trees, + programs, + failures: [...failures, ...globals], + globalFailures: globals, + timing: { runs: 3, maxGrowth: 0.02, rows, groups: {} }, + elapsedMs: 10, + }); + const identical = (id: string) => ({ id, verdict: "identical", artifacts: {} }); + const fast = shard([identical("bench/a")], [row("bench/a", 1.0)]); + // Alone, this shard's bench is slower; with the other shard it is not. + const slow = shard( + [ + { + id: "corpus/x.ts", + verdict: "differs", + artifacts: { exe: { identical: false, first: "resized f" } }, + }, + ], + [row("bench/b", 1.03)], + ["corpus/x.ts: differs"], + ["bench compile time grew +3.00% (CI low +3.00%)"], + ); + const { report, timingLines } = mergeReports([fast, slow]); + expect(report.counts).toEqual({ identical: 1, differs: 1 }); + expect(report.failures).toEqual(["corpus/x.ts: differs"]); + expect(report.verdict).toBe("fail"); + expect(timingLines).toEqual([timingVerdict("bench", report.timing.rows, 0.02).line]); + expect(timingVerdict("bench", report.timing.rows, 0.02).fails).toBe(false); + expect(programLines(report.programs[1])).toEqual(["DIFFERS corpus/x.ts exe: resized f"]); + expect(mergeReports([fast]).report.verdict).toBe("pass"); + expect(mergeReports([shard([], [])]).report.failures).toEqual([ + "no program built with both checkouts, so nothing was compared", + ]); + expect(() => mergeReports([fast, fast])).toThrow(/more than one gate report/); +});