mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -216,9 +216,24 @@ jobs:
|
||||
# Start the corpus immediately beside a serial side queue. One shared
|
||||
# worker budget covers both processes; file and case shard axes stay
|
||||
# separate so every fixture still runs exactly once in each lane.
|
||||
# The tests run in a transient scope capped below the runner's 16 GB, so
|
||||
# the runner itself keeps memory: past 15 GB of RAM the scope swaps, as
|
||||
# the whole runner did before, and when RAM and swap run out the kernel
|
||||
# kills the scope's largest process. A sanitized shard peaks near 16 GB,
|
||||
# and the sanitized self-hosting native-frontend build alone needs 15 GB
|
||||
# (its clang compile of the frontend takes 10.5 GB).
|
||||
# OOMPolicy=continue leaves the rest of the scope running, so vitest
|
||||
# reports the killed worker and the job log names the test. Node sizes
|
||||
# its default heap from the memory it may use, which the scope lowers
|
||||
# below 4 GB; the workers keep the 4 GB heap they had on the bare
|
||||
# runner.
|
||||
- name: Tests (corpus and integration suites)
|
||||
run: |
|
||||
ulimit -c 0
|
||||
if [ -d /run/systemd/system ]; then
|
||||
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue \
|
||||
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts/ci-test.mjs
|
||||
fi
|
||||
node scripts/ci-test.mjs
|
||||
|
||||
tests_macos:
|
||||
@@ -524,6 +539,8 @@ jobs:
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (tsgo and tsc-ts give the same IR)
|
||||
runs-on: ubuntu-24.04
|
||||
# The two corpus compiles run back to back and take close to 30 minutes on a
|
||||
# slow hosted runner, so a 30-minute limit cancels otherwise healthy runs.
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -618,8 +635,9 @@ jobs:
|
||||
# Shard 1 additionally runs package/install and link-parity contracts
|
||||
# before its LLVM differential slice. The slice itself can take ~10
|
||||
# minutes on a cold hosted runner, so 20 minutes cancels an otherwise
|
||||
# healthy job before the differential completes.
|
||||
timeout-minutes: 35
|
||||
# healthy job before the differential completes. Slow runners need more
|
||||
# headroom than that, so the limit is 50 minutes.
|
||||
timeout-minutes: 50
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Prune caches
|
||||
|
||||
# Pull request caches are scoped to that pull request, so once it is closed no
|
||||
# run can use them again. Deleting them keeps the repository under its cache
|
||||
# size limit, so caches that main and open pull requests use are not evicted.
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
prune:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Delete caches of closed pull requests
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Read both lists in full before deleting anything: deleting while the cache
|
||||
# listing pages shifts later pages, and a failed request must stop the job
|
||||
# instead of leaving open pull requests out of the list.
|
||||
open=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number')
|
||||
caches=$(gh api --paginate "repos/$REPO/actions/caches?per_page=100" \
|
||||
--jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"')
|
||||
while read -r id ref; do
|
||||
[ -n "$id" ] || continue
|
||||
number=$(echo "$ref" | cut -d/ -f3)
|
||||
if ! grep -qx "$number" <<<"$open"; then
|
||||
gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null
|
||||
echo "deleted cache $id ($ref)"
|
||||
fi
|
||||
done <<<"$caches"
|
||||
@@ -100,10 +100,59 @@ jobs:
|
||||
if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`);
|
||||
NODE
|
||||
|
||||
build-native-packages:
|
||||
name: Build native package (${{ matrix.platform }})
|
||||
# Collects the native compiler's profile on this commit
|
||||
# (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes
|
||||
# with a profile of the code it compiles. The committed profile falls behind
|
||||
# main within a day or two. If this job fails, the CLI builds use the
|
||||
# committed profile.
|
||||
cli-profile:
|
||||
name: Collect the native compiler profile
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/zig
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: linux-x64-gnu
|
||||
- run: pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
- name: Check out tsc-ts
|
||||
run: |
|
||||
git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts"
|
||||
echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)"
|
||||
npm ci --prefix "$RUNNER_TEMP/tsc-ts"
|
||||
- name: Collect the profile
|
||||
run: >-
|
||||
node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
build-native-packages:
|
||||
name: Build native package (${{ matrix.platform }})
|
||||
needs: [check-release, cli-profile]
|
||||
# Runs when the profile job fails; the CLI builds then use the committed profile.
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' &&
|
||||
needs.check-release.outputs.reuse_artifacts != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -243,6 +292,28 @@ jobs:
|
||||
- name: Build host helper for cross-libc bootstrap
|
||||
if: matrix.seed_helper != ''
|
||||
run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native
|
||||
- name: Download the profile collected on this commit
|
||||
id: cli-profile
|
||||
if: matrix.cli != ''
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile
|
||||
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and
|
||||
# build-native-cli.mts uses the committed profile.
|
||||
- name: Select the native compiler profile
|
||||
if: matrix.cli != ''
|
||||
shell: bash
|
||||
env:
|
||||
COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }}
|
||||
run: |
|
||||
if [ -n "$COLLECTED" ]; then
|
||||
echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV"
|
||||
echo "The native CLI builds with the profile collected on $GITHUB_SHA"
|
||||
else
|
||||
echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile"
|
||||
fi
|
||||
- name: Build native CLI distribution
|
||||
if: matrix.cli != ''
|
||||
env:
|
||||
@@ -558,6 +629,21 @@ jobs:
|
||||
- name: Package standalone compilers
|
||||
run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets
|
||||
|
||||
# The profile the native CLIs were built with. Runs that reuse native
|
||||
# builds from before the cli-profile job, or whose profile job failed,
|
||||
# have none; their CLIs used the committed profile.
|
||||
- name: Download the native compiler profile
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUN: ${{ needs.check-release.outputs.artifact_run }}
|
||||
VERSION: ${{ needs.check-release.outputs.version }}
|
||||
run: |
|
||||
if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then
|
||||
mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz"
|
||||
else
|
||||
echo "Run $RUN has no cli-profile artifact"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
|
||||
@@ -9,6 +9,7 @@ node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/coverage-fixtures/node-islands*/node_modules/
|
||||
!tests/coverage-fixtures/dynamic-tier/package/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
|
||||
+25
-2
@@ -6,6 +6,31 @@ All notable changes to scriptc will be documented in this file.
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Fixes for 0.4.3:**
|
||||
- Unchanged rebuilds hit the frontend cache again. An unchanged rebuild of a 23-module program takes 93 ms instead of 1.26 s, and of tsc-ts (dev) 1.6 s instead of 58.2 s.
|
||||
- The native compiler builds programs with several typed-array accesses at one masked index in a loop again, including tsc-ts's release build, which failed with SC3004.
|
||||
- `x!` returned from a function, used as an arrow function's result, or passed as an argument keeps null or undefined when the destination type holds it, as JavaScript does. `x!` operands of `+`, `+=` and other conversions that hold undefined or null convert as Node does.
|
||||
- Effect 4 programs whose generators yield clients built by other generators run again.
|
||||
- The cache for untyped property access in programs that declare classes is reverted, fixing a crash in Effect 4 observability programs.
|
||||
- **Faster:**
|
||||
- Number compares and typed-array indexes use the exact integer of sums like `i + 1`: a heap sift-down over an `Int32Array` runs 17.5% faster.
|
||||
- glibc's per-thread heaps use huge pages: the VS Code type-check runs 3.4% faster.
|
||||
- Mid-size executables (over 1 MiB of LLVM text) build in at least eight partitions: the LLVM backend phase takes 34 to 74% less time for three.js programs of 1.4 to 7.8 MB.
|
||||
- JavaScript class instances are boxed at their exact class, and stores into nested string-keyed dictionaries keep class method calls direct.
|
||||
- `--target-cpu` selects an x86-64 microarchitecture level.
|
||||
- **Node.js parity:**
|
||||
- X25519 keys and `crypto.diffieHellman`; `existsSync` and `fs.promises` accept `file:` URLs; `Buffer.from` ranges over ArrayBuffers, the deprecated `Buffer()` constructor and `Buffer` as a value.
|
||||
- Dates are objects with identity; `Promise.all` over different entry types settles on Node's turns; completion callbacks run in their call's AsyncLocalStorage context.
|
||||
- TypedArray methods run on typed arrays held as untyped values, and unions of typed-array classes compile. Borrowed `TypedArray.prototype.set` calls compile and throw Node's TypeError for other targets.
|
||||
- Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built.
|
||||
- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Highlights
|
||||
@@ -26,8 +51,6 @@ All notable changes to scriptc will be documented in this file.
|
||||
- **npm packages:** zod 3 compiles natively and zod 4's core runs, minified packages such as marked compile, chalk 5 and hot-shots 8 build from built-in shims, and `@hono/node-server` 2.x is built in. User code types npm package declarations at a checked, identity-preserving boundary.
|
||||
- **Builds:** the cached build is reused after whitespace and comment edits, function bodies are emitted on forked workers, and the LLVM emitter reads its analyses from one recorded walk of each module.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Highlights
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ CI (`.github/workflows/release.yml`) compares the version in `packages/cli/packa
|
||||
|
||||
The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds.
|
||||
|
||||
Release builds of the native CLI optimize the compiler with the committed profile `packages/compiler/native/cli.profdata` on every platform. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. Refresh the profile on a quiet Linux x64 host when compiler code has changed substantially since the last refresh: `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Commit the new profile on its own.
|
||||
Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc-<version>-cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` on a quiet Linux x64 host and commit the new profile on its own.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
|
||||
Each published package must have a GitHub Actions trusted publisher for
|
||||
|
||||
@@ -10,12 +10,17 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
|
||||
| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
|
||||
@@ -25,12 +30,15 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
|
||||
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
|
||||
| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves |
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/date-math.ts",
|
||||
"args": [
|
||||
"40"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
|
||||
// UTC and local setters, weekday and month-end rules read back through the
|
||||
// getters, Dates compared and sorted by time value, and ISO text for a
|
||||
// sample. Argument: the number of rounds.
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "40");
|
||||
let seed = 2026;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
/** The next business day at 09:00 local time on or after `from`. */
|
||||
function nextBusinessDay(from: Date): Date {
|
||||
const day = new Date(from.getTime());
|
||||
day.setHours(9, 0, 0, 0);
|
||||
if (day < from) day.setDate(day.getDate() + 1);
|
||||
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
|
||||
return day;
|
||||
}
|
||||
|
||||
/** The last day of `date`'s month, in UTC. */
|
||||
function monthEnd(date: Date): number {
|
||||
const end = new Date(date.getTime());
|
||||
end.setUTCDate(1);
|
||||
end.setUTCMonth(end.getUTCMonth() + 1);
|
||||
end.setUTCDate(0);
|
||||
return end.getUTCDate();
|
||||
}
|
||||
|
||||
let checksum = 0;
|
||||
let sample = "";
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const due: Date[] = [];
|
||||
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
|
||||
const next = nextBusinessDay(cursor);
|
||||
checksum += next.getDate() + monthEnd(next) + next.getHours();
|
||||
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
|
||||
due.push(next);
|
||||
}
|
||||
due.sort((a, b) => +a - +b);
|
||||
let previous = due[0]!;
|
||||
for (const date of due) {
|
||||
checksum += (+date - +previous) / 3600000;
|
||||
previous = date;
|
||||
}
|
||||
sample = due[due.length >> 1]!.toISOString();
|
||||
}
|
||||
console.log(Math.round(checksum), sample);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-properties.mjs",
|
||||
"args": [
|
||||
"1000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Untyped JavaScript reading and writing properties of values it never
|
||||
// declares types for: records built by object literals in two key orders,
|
||||
// nested records, property updates, and instances of a class and its
|
||||
// subclass read and written the same way. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "1000000");
|
||||
class Cursor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
step(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, y, label) {
|
||||
super(x, y);
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
function makeItem(i) {
|
||||
if (i % 3 === 0)
|
||||
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
|
||||
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
|
||||
}
|
||||
function cost(item) {
|
||||
return item.price * item.qty + item.meta.weight;
|
||||
}
|
||||
function restock(item, round) {
|
||||
item.qty = (item.qty + round) % 7;
|
||||
item.meta.tag = (item.meta.tag + 1) % 7;
|
||||
}
|
||||
function distance(a, b) {
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
|
||||
const cursors = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
|
||||
const origin = { x: 0, y: 0 };
|
||||
let checksum = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
const item = items[r & 1023];
|
||||
checksum += cost(item);
|
||||
restock(item, r & 3);
|
||||
checksum += item.meta.tag;
|
||||
const cursor = cursors[r & 63];
|
||||
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
|
||||
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
|
||||
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
|
||||
}
|
||||
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/json-unions.ts",
|
||||
"args": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// JSON text parsed into discriminated unions: an event log with eight kinds
|
||||
// of event and a nested expression tree with nine kinds of node, cast with
|
||||
// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the
|
||||
// number of rounds.
|
||||
type Event =
|
||||
| { kind: "click"; target: string; x: number; y: number }
|
||||
| { key: string; kind: "key"; repeat: boolean }
|
||||
| { kind: "scroll"; offset: number }
|
||||
| { kind: "resize"; height: number; width: number }
|
||||
| { kind: "focus"; target: string }
|
||||
| { kind: "blur"; target: string }
|
||||
| { kind: "load"; ms: number; url: string }
|
||||
| { code: number; kind: "error"; message: string };
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "var"; name: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "sub"; left: Expr; right: Expr }
|
||||
| { kind: "mul"; left: Expr; right: Expr }
|
||||
| { kind: "max"; items: Expr[] }
|
||||
| { else: Expr; kind: "if"; test: Expr; then: Expr }
|
||||
| { body: Expr; kind: "let"; name: string; value: Expr };
|
||||
|
||||
let seed = 12345;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
const targets = ["menu", "search", "button", "list", "footer"];
|
||||
function eventText(i: number): string {
|
||||
const target = targets[i % targets.length]!;
|
||||
switch (i % 8) {
|
||||
case 0:
|
||||
return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`;
|
||||
case 1:
|
||||
return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`;
|
||||
case 2:
|
||||
return `{"kind":"scroll","offset":${(i * 7) % 2000}}`;
|
||||
case 3:
|
||||
return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`;
|
||||
case 4:
|
||||
return `{"kind":"focus","target":"${target}"}`;
|
||||
case 5:
|
||||
return `{"kind":"blur","target":"${target}"}`;
|
||||
case 6:
|
||||
return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`;
|
||||
default:
|
||||
return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`;
|
||||
}
|
||||
}
|
||||
|
||||
const names = ["a", "b", "c", "d"];
|
||||
function exprText(depth: number): string {
|
||||
const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9);
|
||||
switch (pick) {
|
||||
case 0:
|
||||
return `{"kind":"num","value":${Math.floor(random() * 100)}}`;
|
||||
case 1:
|
||||
return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`;
|
||||
case 2:
|
||||
return `{"kind":"neg","operand":${exprText(depth - 1)}}`;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5: {
|
||||
const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul";
|
||||
return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`;
|
||||
}
|
||||
case 6:
|
||||
return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`;
|
||||
case 7:
|
||||
return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`;
|
||||
default:
|
||||
return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr, env: Map<string, number>): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "var":
|
||||
return env.get(e.name) ?? 1;
|
||||
case "neg":
|
||||
return -evaluate(e.operand, env);
|
||||
case "add":
|
||||
return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003;
|
||||
case "sub":
|
||||
return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003;
|
||||
case "mul":
|
||||
return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003;
|
||||
case "max": {
|
||||
let best = -Infinity;
|
||||
for (const item of e.items) best = Math.max(best, evaluate(item, env));
|
||||
return best;
|
||||
}
|
||||
case "if":
|
||||
return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env);
|
||||
case "let": {
|
||||
const inner = new Map(env);
|
||||
inner.set(e.name, evaluate(e.value, env));
|
||||
return evaluate(e.body, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "6");
|
||||
const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`;
|
||||
const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`;
|
||||
let clicks = 0;
|
||||
let keys = 0;
|
||||
let scrolled = 0;
|
||||
let area = 0;
|
||||
let loadMs = 0;
|
||||
let errors = 0;
|
||||
let focus = 0;
|
||||
let total = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const events = JSON.parse(eventsText) as Event[];
|
||||
for (const event of events) {
|
||||
switch (event.kind) {
|
||||
case "click":
|
||||
clicks += event.x + event.y;
|
||||
break;
|
||||
case "key":
|
||||
if (event.repeat) keys++;
|
||||
break;
|
||||
case "scroll":
|
||||
scrolled += event.offset;
|
||||
break;
|
||||
case "resize":
|
||||
area += (event.width * event.height) % 1000;
|
||||
break;
|
||||
case "focus":
|
||||
case "blur":
|
||||
focus += event.target.length;
|
||||
break;
|
||||
case "load":
|
||||
loadMs += event.ms;
|
||||
break;
|
||||
case "error":
|
||||
errors += event.code % 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const trees = JSON.parse(treesText) as Expr[];
|
||||
const env = new Map([
|
||||
["a", 3],
|
||||
["b", 5],
|
||||
]);
|
||||
for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003;
|
||||
}
|
||||
console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/keyed-option-writes.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Class methods whose names are also keys of option records written through
|
||||
// element accesses: `enabled[name] = true` over a union of literal names and
|
||||
// `totals["count"] = n`. The writes store booleans and numbers into plain
|
||||
// records, so the methods keep their native calls.
|
||||
type Step = "scale" | "offset" | "clamp";
|
||||
|
||||
const enabled: Record<Step, boolean> = { scale: false, offset: false, clamp: false };
|
||||
function enable(name: Step): void {
|
||||
enabled[name] = true;
|
||||
}
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
|
||||
class Transform {
|
||||
private readonly factor: number;
|
||||
private readonly shift: number;
|
||||
constructor(factor: number, shift: number) {
|
||||
this.factor = factor;
|
||||
this.shift = shift;
|
||||
}
|
||||
scale(x: number): number {
|
||||
return enabled.scale ? x * this.factor : x;
|
||||
}
|
||||
offset(x: number): number {
|
||||
return enabled.offset ? x + this.shift : x;
|
||||
}
|
||||
clamp(x: number): number {
|
||||
if (!enabled.clamp) return x;
|
||||
return x < 0 ? 0 : x > 1000 ? 1000 : x;
|
||||
}
|
||||
}
|
||||
|
||||
class Counter {
|
||||
private seen = 0;
|
||||
private sum = 0;
|
||||
add(value: number): void {
|
||||
this.seen++;
|
||||
this.sum += value;
|
||||
}
|
||||
count(): number {
|
||||
return this.seen;
|
||||
}
|
||||
mean(): number {
|
||||
return this.seen === 0 ? 0 : this.sum / this.seen;
|
||||
}
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const steps: Step[] = ["scale", "offset", "clamp"];
|
||||
for (const step of steps) enable(step);
|
||||
|
||||
const transforms: Transform[] = [];
|
||||
for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3));
|
||||
const counters: Counter[] = [];
|
||||
for (let i = 0; i < 8; i++) counters.push(new Counter());
|
||||
|
||||
let seed = 12345;
|
||||
let checksum = 0;
|
||||
const rounds = Math.floor(2_000_000 * scale);
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
const t = transforms[seed & 15]!;
|
||||
const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200)));
|
||||
const counter = counters[(seed >>> 4) & 7]!;
|
||||
counter.add(value);
|
||||
checksum += value;
|
||||
if ((i & 65535) === 0) totals["count"] = counter.count();
|
||||
}
|
||||
|
||||
let mean = 0;
|
||||
let seen = 0;
|
||||
for (const counter of counters) {
|
||||
mean += counter.mean();
|
||||
seen += counter.count();
|
||||
}
|
||||
console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"entry": "workloads/line-reader.ts",
|
||||
"input": "line-reader",
|
||||
"args": [
|
||||
"{input}"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Line-by-line processing of a log file with readline over a file stream:
|
||||
// 'line' events for about 67 MB of access-log lines, with light work per
|
||||
// line (a status count, a path tally, byte totals).
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
const path = process.argv[2]!;
|
||||
const statuses = new Map<string, number>();
|
||||
const paths = new Map<string, number>();
|
||||
let lines = 0;
|
||||
let chars = 0;
|
||||
let bytes = 0;
|
||||
let longest = 0;
|
||||
|
||||
const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
|
||||
input.on("line", (line: string) => {
|
||||
lines++;
|
||||
chars += line.length;
|
||||
if (line.length > longest) longest = line.length;
|
||||
const quote = line.indexOf('"');
|
||||
const space = line.indexOf(" ", quote);
|
||||
const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1));
|
||||
paths.set(path, (paths.get(path) ?? 0) + 1);
|
||||
const close = line.indexOf('"', space);
|
||||
const status = line.slice(close + 2, close + 5);
|
||||
statuses.set(status, (statuses.get(status) ?? 0) + 1);
|
||||
bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6)));
|
||||
});
|
||||
input.on("close", () => {
|
||||
console.log(`${lines} lines, ${chars} characters, longest ${longest}`);
|
||||
console.log(`bytes ${bytes}`);
|
||||
for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`);
|
||||
for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/regex-replacers.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Regex replace and replaceAll with inline callbacks, written as they
|
||||
// usually are, without parameter annotations: template placeholders looked
|
||||
// up in a dictionary, snake_case to camelCase, date reordering, optional
|
||||
// version parts, capitalization and a string search with its offset.
|
||||
let seed = 7;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(random() * items.length)]!;
|
||||
}
|
||||
|
||||
const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"];
|
||||
const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"];
|
||||
const vars: Record<string, string> = {
|
||||
name: "world",
|
||||
city: "Lisbon",
|
||||
day: "Tuesday",
|
||||
plan: "pro",
|
||||
count: "12",
|
||||
};
|
||||
const keys = [...Object.keys(vars), "missing"];
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const count = Math.floor(20000 * scale);
|
||||
const templates: string[] = [];
|
||||
const identifiers: string[] = [];
|
||||
const dates: string[] = [];
|
||||
const versions: string[] = [];
|
||||
const sentences: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`);
|
||||
identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`);
|
||||
const year = 1990 + Math.floor(random() * 40);
|
||||
const month = String(1 + Math.floor(random() * 12)).padStart(2, "0");
|
||||
const day = String(1 + Math.floor(random() * 28)).padStart(2, "0");
|
||||
dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`);
|
||||
versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`);
|
||||
sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`);
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
let checksum = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`);
|
||||
const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
||||
const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`);
|
||||
const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`);
|
||||
const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
||||
const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m));
|
||||
length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length;
|
||||
checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0;
|
||||
}
|
||||
}
|
||||
console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`));
|
||||
console.log("length", length, "checksum", checksum);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/throw-chains.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Exceptions that cross many frames: throws from helpers below a recursion,
|
||||
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
|
||||
// callback), with each frame holding an array across its call, and a
|
||||
// backtracking parser that uses exceptions for control flow.
|
||||
class ParseError extends Error {
|
||||
readonly at: number;
|
||||
constructor(at: number, message: string) {
|
||||
super(message);
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
/** A thrown value that is not an Error (no stack to capture). */
|
||||
class Stop {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const held: number[][] = [];
|
||||
function fail(n: number, plain: boolean): number {
|
||||
if (plain) throw new Stop(n);
|
||||
throw new RangeError("chain " + n);
|
||||
}
|
||||
function check(n: number, plain: boolean): number {
|
||||
return (n & 3) === 0 ? fail(n, plain) : n;
|
||||
}
|
||||
function validate(n: number, plain: boolean): number {
|
||||
return check(n, plain) + (n & 1);
|
||||
}
|
||||
function bottom(n: number, plain: boolean): number {
|
||||
const box = [n, -1];
|
||||
const r = validate(n, plain);
|
||||
held[0] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function walk(n: number, depth: number, plain: boolean): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
|
||||
function chains(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
for (const depth of [2, 5, 20])
|
||||
for (const plain of [false, true]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += walk(i, depth - 2, plain);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof Stop ? error.code & 7 : 1;
|
||||
}
|
||||
}
|
||||
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// The same chains through a callback: each level visits the next with a
|
||||
// closure, as tree visitors do.
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function visits(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (const depth of [2, 5, 20]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += visit(i, depth - 2, each);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof RangeError ? 1 : 0;
|
||||
}
|
||||
}
|
||||
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
|
||||
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
|
||||
// A call and a name start alike: the parser tries the call first and
|
||||
// backtracks when it fails.
|
||||
let text = "";
|
||||
let pos = 0;
|
||||
function peek(): string {
|
||||
return pos < text.length ? text[pos]! : "";
|
||||
}
|
||||
function expect(c: string): void {
|
||||
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
|
||||
pos++;
|
||||
}
|
||||
function name(): string {
|
||||
const start = pos;
|
||||
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
|
||||
if (pos === start) throw new ParseError(pos, "expected a name");
|
||||
return text.slice(start, pos);
|
||||
}
|
||||
function call(): number {
|
||||
const callee = name();
|
||||
expect("(");
|
||||
let value = expr() + callee.length;
|
||||
while (peek() === ",") {
|
||||
pos++;
|
||||
value += expr();
|
||||
}
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
function atom(): number {
|
||||
const c = peek();
|
||||
if (c >= "0" && c <= "9") {
|
||||
let value = 0;
|
||||
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
|
||||
return value;
|
||||
}
|
||||
if (c === "(") {
|
||||
pos++;
|
||||
const value = expr();
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
const start = pos;
|
||||
try {
|
||||
return call();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
pos = start;
|
||||
return name().length;
|
||||
}
|
||||
}
|
||||
function term(): number {
|
||||
let value = atom();
|
||||
while (peek() === "*") {
|
||||
pos++;
|
||||
value = (value * atom()) % 1000003;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function expr(): number {
|
||||
let value = term();
|
||||
while (peek() === "+" || peek() === "-") {
|
||||
const op = text[pos++];
|
||||
value = op === "+" ? value + term() : value - term();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
let seed = 4242;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function source(depth: number): string {
|
||||
const r = random();
|
||||
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
|
||||
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
|
||||
if (r < 0.6) return `(${source(depth + 1)})`;
|
||||
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
|
||||
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
|
||||
}
|
||||
|
||||
function parse(count: number): string {
|
||||
let total = 0;
|
||||
let failed = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Every tenth input lacks its last character, which fails deep inside it.
|
||||
const full = source(0);
|
||||
text = i % 10 === 0 ? full.slice(0, -1) : full;
|
||||
pos = 0;
|
||||
try {
|
||||
total = (total + expr()) % 1000003;
|
||||
if (pos !== text.length) throw new ParseError(pos, "trailing input");
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return `parsed ${count}: total ${total} failed ${failed}`;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
console.log(chains(Math.floor(60000 * scale)));
|
||||
console.log(visits(Math.floor(60000 * scale)));
|
||||
console.log(parse(Math.floor(40000 * scale)));
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/union-node-sets.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Syntax-tree passes over a discriminated union of object literals, the way
|
||||
// compilers walk their IR: a DAG of shared subexpressions, a `Set` of
|
||||
// visited nodes, a `Map` from node to its value, and conversions between
|
||||
// the whole union and the sub-unions passes narrow it to.
|
||||
type Num = { kind: "num"; value: number };
|
||||
type Var = { kind: "var"; slot: number };
|
||||
type Neg = { kind: "neg"; operand: Node };
|
||||
type Add = { kind: "add"; left: Node; right: Node };
|
||||
type Mul = { kind: "mul"; left: Node; right: Node };
|
||||
type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node };
|
||||
type Leaf = Num | Var;
|
||||
type Node = Leaf | Neg | Add | Mul | Pick;
|
||||
|
||||
function children(node: Node, visit: (child: Node) => void): void {
|
||||
switch (node.kind) {
|
||||
case "num":
|
||||
case "var":
|
||||
return;
|
||||
case "neg":
|
||||
visit(node.operand);
|
||||
return;
|
||||
case "add":
|
||||
case "mul":
|
||||
visit(node.left);
|
||||
visit(node.right);
|
||||
return;
|
||||
case "pick":
|
||||
visit(node.cond);
|
||||
visit(node.then);
|
||||
visit(node.else_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function leafValue(leaf: Leaf, env: number[]): number {
|
||||
return leaf.kind === "num" ? leaf.value : env[leaf.slot]!;
|
||||
}
|
||||
|
||||
function evaluate(node: Node, env: number[], memo: Map<Node, number>): number {
|
||||
const cached = memo.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
let result: number;
|
||||
switch (node.kind) {
|
||||
case "num":
|
||||
case "var":
|
||||
result = leafValue(node, env);
|
||||
break;
|
||||
case "neg":
|
||||
result = -evaluate(node.operand, env, memo);
|
||||
break;
|
||||
case "add":
|
||||
result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003;
|
||||
break;
|
||||
case "mul":
|
||||
result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003;
|
||||
break;
|
||||
case "pick":
|
||||
result =
|
||||
evaluate(node.cond, env, memo) % 2 === 0
|
||||
? evaluate(node.then, env, memo)
|
||||
: evaluate(node.else_, env, memo);
|
||||
break;
|
||||
}
|
||||
memo.set(node, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function reachable(root: Node): number {
|
||||
const seen = new Set<Node>();
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (seen.has(node)) continue;
|
||||
seen.add(node);
|
||||
children(node, (child) => {
|
||||
if (!seen.has(child)) stack.push(child);
|
||||
});
|
||||
}
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
function leaves(root: Node): Leaf[] {
|
||||
const out: Leaf[] = [];
|
||||
const seen = new Set<Node>();
|
||||
const walk = (node: Node): void => {
|
||||
if (seen.has(node)) return;
|
||||
seen.add(node);
|
||||
if (node.kind === "num" || node.kind === "var") out.push(node);
|
||||
else children(node, walk);
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
let seed = 2463534242;
|
||||
function next(): number {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >>> 17;
|
||||
seed ^= seed << 5;
|
||||
return seed >>> 0;
|
||||
}
|
||||
|
||||
function build(size: number): Node {
|
||||
const pool: Node[] = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 });
|
||||
// Operands come from the most recent nodes, so most of the DAG is
|
||||
// reachable from the root and many nodes are shared.
|
||||
const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!;
|
||||
for (let i = 0; i < size; i++) {
|
||||
const a = recent();
|
||||
const b = recent();
|
||||
const r = next() % 10;
|
||||
let node: Node;
|
||||
if (r < 1) node = { kind: "neg", operand: a };
|
||||
else if (r < 5) node = { kind: "add", left: a, right: b };
|
||||
else if (r < 8) node = { kind: "mul", left: a, right: b };
|
||||
else node = { kind: "pick", cond: a, then: b, else_: recent() };
|
||||
pool.push(node);
|
||||
}
|
||||
return pool[pool.length - 1]!;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const rounds = Math.max(1, Math.round(40 * scale));
|
||||
let checksum = 0;
|
||||
let nodes = 0;
|
||||
let leafCount = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const root = build(20_000);
|
||||
nodes += reachable(root);
|
||||
const found = leaves(root);
|
||||
leafCount += found.length;
|
||||
for (let pass = 0; pass < 4; pass++) {
|
||||
const env: number[] = [];
|
||||
for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97);
|
||||
checksum = (checksum + evaluate(root, env, new Map())) % 1000003;
|
||||
for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003;
|
||||
}
|
||||
}
|
||||
console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum);
|
||||
@@ -210,6 +210,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER</code></dt>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER_SERVER</code></dt>
|
||||
<dd>Set to <code>1</code> on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves <code>tsc-ts --api --listen</code>. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (<code>TSC_TS_LISTEN_MEMORY</code> sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
|
||||
@@ -245,7 +245,7 @@ $ scriptc build scale.ts
|
||||
1 error.
|
||||
```
|
||||
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
|
||||
## Immutable
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ The LLVM helper divides an executable's LLVM IR into partitions by its size. Up
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. A function's partition follows from its name, and a small internal function goes with the one function that calls it, so an edit leaves the other functions in their partitions.
|
||||
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition.
|
||||
|
||||
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
|
||||
|
||||
|
||||
@@ -220,6 +220,14 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/tls-ciphers.mdx</include>
|
||||
|
||||
### TLS certificates
|
||||
|
||||
<include>../limitations/tls-certificates.mdx</include>
|
||||
|
||||
### TLS sessions
|
||||
|
||||
<include>../limitations/tls-sessions.mdx</include>
|
||||
|
||||
### Console and process streams
|
||||
|
||||
<include>../limitations/console-and-process-streams.mdx</include>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`.
|
||||
Values whose type can be cyclic, such as recursive records, also read as `undefined`.
|
||||
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
|
||||
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
|
||||
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
|
||||
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
|
||||
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
|
||||
Checked-dynamic values compared deeply have no cycle detection.
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`.
|
||||
Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer.
|
||||
`zlib.constants` members and their module-level copies read as Node's values.
|
||||
`util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported.
|
||||
The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node.
|
||||
`util.inspect` and Zstd compression are unsupported.
|
||||
|
||||
@@ -31,18 +31,25 @@
|
||||
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
|
||||
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
|
||||
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
|
||||
RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
|
||||
RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
|
||||
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
|
||||
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
|
||||
RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
|
||||
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
|
||||
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
|
||||
EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
|
||||
Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
|
||||
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
|
||||
`crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
|
||||
RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error.
|
||||
Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
|
||||
X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does.
|
||||
`publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations.
|
||||
Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters.
|
||||
`JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused.
|
||||
Sign and Verify values accumulate `update()` data and are not writable streams.
|
||||
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
|
||||
Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error.
|
||||
Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given.
|
||||
The other key types (`dsa`, `ed448`, `x448`, and the rest) and wasm32-wasi builds throw a coded SC2020 error or are refused.
|
||||
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
|
||||
`publicEncrypt`/`privateDecrypt` remain unsupported.
|
||||
The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
|
||||
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Unions of a Date with other non-nullish types are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
- **Date:** Spread constructor arguments and locale formatters are unsupported.
|
||||
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
|
||||
A Date converted to `unknown` is the same Date object.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile.
|
||||
Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale).
|
||||
In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error.
|
||||
Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class.
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`.
|
||||
`Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
|
||||
The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
|
||||
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
|
||||
Class constructors are not supported.
|
||||
A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#<Object>'` (or `of function '…'`) suffix.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
|
||||
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
|
||||
The inherited methods and accessors run with the inheriting object as `this`.
|
||||
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
|
||||
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
|
||||
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
|
||||
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
`instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
|
||||
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
|
||||
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
|
||||
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
|
||||
The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
|
||||
Later `pipeline` stages and `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
|
||||
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
|
||||
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
|
||||
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
|
||||
@@ -14,7 +15,7 @@
|
||||
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
|
||||
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
The source may also be an array, a string, a `Set`, a `Map`, a generator or async generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
Stages may take the `{ signal }` argument.
|
||||
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
|
||||
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
- **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup.
|
||||
Mapper-less checked `Array.from` accepts those iterators and plain array-like objects.
|
||||
Sparse checked arrays and general borrowed iterator factories are unsupported.
|
||||
Direct statically typed class loops also restrict iterator `return` and `throw` methods.
|
||||
Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
- **URL:** Non-ASCII or percent-escaped hosts are unsupported.
|
||||
Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
|
||||
`URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
|
||||
`href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
|
||||
Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits.
|
||||
Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused.
|
||||
`origin` is read-only.
|
||||
`searchParams` is a live view whose identity survives query and URL replacement.
|
||||
Opaque paths retain their input bytes without WHATWG C0 encoding.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported.
|
||||
In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`.
|
||||
`Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`.
|
||||
`Immutable` refuses values stored in `any` or `unknown` or that another reference can still change.
|
||||
`ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported.
|
||||
Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`.
|
||||
`for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record.
|
||||
A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`.
|
||||
Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options.
|
||||
The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options.
|
||||
`Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported.
|
||||
|
||||
@@ -2,5 +2,12 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro
|
||||
Direct `createRequire` calls to local addons are rejected at compile time.
|
||||
A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI.
|
||||
|
||||
A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block.
|
||||
There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error.
|
||||
Its `require.resolve` read as a value is a function that throws when called.
|
||||
Coverage lists these as deferred to runtime.
|
||||
The package's `catch` clause then runs as it does in Node.js when the addon is missing.
|
||||
Outside a `try` block, the same uses keep the package refused.
|
||||
|
||||
Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive.
|
||||
The guide includes a replacement for a small Node-API helper.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known.
|
||||
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
|
||||
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
|
||||
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
|
||||
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
|
||||
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
|
||||
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
|
||||
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused.
|
||||
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
|
||||
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
|
||||
The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves.
|
||||
OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason.
|
||||
A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so.
|
||||
The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields.
|
||||
@@ -0,0 +1,3 @@
|
||||
TLS servers send no session tickets, so a connection to them cannot be resumed.
|
||||
A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`.
|
||||
Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two.
|
||||
@@ -1,4 +1,5 @@
|
||||
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
|
||||
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
|
||||
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
|
||||
JavaScript object spreads copy represented own symbol fields.
|
||||
Equal registry keys share a field across modules.
|
||||
|
||||
@@ -13822,9 +13822,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13845,9 +13845,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13868,9 +13868,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13891,9 +13891,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13914,9 +13914,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13937,9 +13937,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13960,9 +13960,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13983,9 +13983,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14006,9 +14006,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14029,9 +14029,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14236,9 +14236,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -18268,9 +18268,9 @@
|
||||
"anchor": "err_brotli_invalid_param",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23790,9 +23790,9 @@
|
||||
"anchor": "err_tls_required_server_name",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67484,9 +67484,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67599,9 +67599,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67783,9 +67783,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67921,9 +67921,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68013,9 +68013,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68059,9 +68059,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68174,9 +68174,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68312,9 +68312,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68335,9 +68335,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71509,9 +71509,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84302,9 +84302,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84325,9 +84325,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84349,7 +84349,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84372,7 +84372,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84395,7 +84395,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84418,7 +84418,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84441,7 +84441,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84464,7 +84464,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84487,7 +84487,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84510,7 +84510,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84533,7 +84533,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84556,7 +84556,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84579,7 +84579,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84602,7 +84602,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84625,7 +84625,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -84716,9 +84716,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84739,9 +84739,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84969,9 +84969,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -84992,9 +84992,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85015,9 +85015,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85038,9 +85038,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85499,7 +85499,7 @@
|
||||
},
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
|
||||
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
@@ -85521,9 +85521,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85567,9 +85567,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85590,9 +85590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85613,9 +85613,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -85636,9 +85636,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19394,6 +19394,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/1358-crypto-random.ts",
|
||||
"tests/corpus/1534-crypto-hash-chain.ts",
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/2556-crypto-introspection.ts",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
"tests/corpus/5250-cipher-aes-gcm.ts",
|
||||
@@ -19436,6 +19438,7 @@
|
||||
"tests/corpus/6709-dh-generate.ts",
|
||||
"tests/corpus/6710-dh-encodings.cjs",
|
||||
"tests/corpus/6711-dh-handles.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts",
|
||||
"tests/corpus/7055-cipher-streams.ts",
|
||||
"tests/corpus/7056-cipher-stream-errors.ts",
|
||||
"tests/corpus/7057-cipher-streams.mjs",
|
||||
@@ -19443,6 +19446,10 @@
|
||||
"tests/corpus/7082-crypto-key-objects.ts",
|
||||
"tests/corpus/7083-crypto-sign-verify.ts",
|
||||
"tests/corpus/7084-crypto-key-pairs.cjs",
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/corpus/7122-webcrypto-node-crypto.ts",
|
||||
"tests/corpus/7123-webcrypto-node-crypto.cjs",
|
||||
"tests/corpus/7150-cipher-legacy-modes.ts",
|
||||
@@ -19461,6 +19468,7 @@
|
||||
"tests/fixtures/node-types/dh.ts",
|
||||
"tests/fixtures/node-types/ecdh.ts",
|
||||
"tests/fixtures/node-types/keys.ts",
|
||||
"tests/fixtures/node-types/x509-methods.ts",
|
||||
"tests/harness/npm-static.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -23784,8 +23792,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:X509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23807,8 +23820,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkEmail",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23830,8 +23850,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkHost",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23853,8 +23880,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIP",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23876,8 +23910,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIssued",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23899,8 +23940,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkPrivateKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23922,8 +23970,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toJSON",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23945,8 +24000,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toLegacyObject",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23968,8 +24030,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toString",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23991,8 +24060,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.verify",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -24198,8 +24274,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:crypto.publicKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -32679,6 +32762,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
"tests/corpus/error-stack-limit.ts",
|
||||
@@ -33471,8 +33556,11 @@
|
||||
"anchor": "err_brotli_invalid_param",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_BROTLI_INVALID_PARAM",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -39008,8 +39096,11 @@
|
||||
"anchor": "err_tls_required_server_name",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -95873,6 +95964,17 @@
|
||||
"tests/corpus/6722-tls-min-max-version.ts",
|
||||
"tests/corpus/7087-tls-socket-wrap.ts",
|
||||
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts",
|
||||
"tests/fixtures/node-types/tls-socket-wrap.ts"
|
||||
]
|
||||
},
|
||||
@@ -96241,8 +96343,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.addContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96368,8 +96476,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.setSecureContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96552,8 +96666,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:'tlsClientError'",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96695,8 +96815,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96787,8 +96912,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96856,8 +96986,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96974,8 +97109,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97115,8 +97255,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.authorizationError",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97138,8 +97284,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:tls.authorized",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -99483,6 +99635,7 @@
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/corpus/1794-searchparams-url-live.ts",
|
||||
"tests/corpus/5220-url-parse-warnings.ts",
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
@@ -100741,8 +100894,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.urlToHttpOptions",
|
||||
"tests": [
|
||||
"tests/corpus/1794-searchparams-url-live.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -115756,7 +115912,10 @@
|
||||
"tests/corpus/5377-zlib-stream-backpressure.cjs",
|
||||
"tests/corpus/5378-zlib-sync-options.cjs",
|
||||
"tests/corpus/5379-zlib-callback-options.ts",
|
||||
"tests/corpus/5415-zlib-dynamic-inputs.cjs"
|
||||
"tests/corpus/5415-zlib-dynamic-inputs.cjs",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -116239,8 +116398,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.zlib.BrotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116262,8 +116424,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.zlib.BrotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116781,8 +116947,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.createBrotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -116804,8 +116973,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.createBrotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117111,8 +117284,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117134,8 +117310,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117157,8 +117336,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -117180,8 +117363,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -118198,8 +118385,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:zlib"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:zlib.BrotliOptions",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -118267,8 +118457,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118290,8 +118483,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118313,8 +118509,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -118336,8 +118536,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
|
||||
"tests": [
|
||||
"tests/corpus/7715-zlib-brotli-decompress.cjs",
|
||||
"tests/corpus/7716-zlib-brotli.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
|
||||
@@ -652,7 +652,14 @@ function publicDetail(tier) {
|
||||
source,
|
||||
)
|
||||
) {
|
||||
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.";
|
||||
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.";
|
||||
}
|
||||
if (
|
||||
/^compiler-feature:zlib\.(?:zlib\.(?:BrotliCompress|BrotliDecompress)|BrotliOptions)$/.test(
|
||||
source,
|
||||
)
|
||||
) {
|
||||
return "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().";
|
||||
}
|
||||
if (source === "compiler-feature:zlib.zlib.constants") {
|
||||
return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value.";
|
||||
|
||||
@@ -190,6 +190,12 @@
|
||||
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
|
||||
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
|
||||
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
|
||||
"node-builtin.zlib.brotliCompressSync": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.brotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.createBrotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
|
||||
"node-builtin.zlib.brotliDecompressSync": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"node-builtin.zlib.brotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"node-builtin.zlib.createBrotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
|
||||
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
|
||||
"stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"],
|
||||
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
|
||||
@@ -202,6 +208,8 @@
|
||||
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
|
||||
},
|
||||
"features": [
|
||||
{ "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
@@ -404,6 +412,9 @@
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_BROTLI_INVALID_PARAM"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliDecompress`"], "status": "partial", "evidence": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
|
||||
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
|
||||
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
|
||||
@@ -427,6 +438,11 @@
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] },
|
||||
{ "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] },
|
||||
@@ -437,6 +453,7 @@
|
||||
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
|
||||
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.ts"] },
|
||||
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
|
||||
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "llvm/CodeGen/MachineFunction.h"
|
||||
#include "llvm/CodeGen/MachineFunctionPass.h"
|
||||
#include "llvm/CodeGen/MachineModuleInfo.h"
|
||||
#include "llvm/IR/DiagnosticHandler.h"
|
||||
#include "llvm/IR/DiagnosticInfo.h"
|
||||
#include "llvm/IR/DiagnosticPrinter.h"
|
||||
#include "llvm/IR/LegacyPassManager.h"
|
||||
@@ -41,11 +42,13 @@
|
||||
#include "llvm/Support/SourceMgr.h"
|
||||
#include "llvm/Support/ThreadPool.h"
|
||||
#include "llvm/Support/Threading.h"
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
#include "llvm/Target/TargetMachine.h"
|
||||
#include "llvm/TargetParser/Triple.h"
|
||||
#include "llvm/Transforms/Utils/SplitModule.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -170,6 +173,108 @@ void limitProfileImports() {
|
||||
(void)Limited;
|
||||
}
|
||||
|
||||
// How the --profile-use profile matched the functions it annotated, summed
|
||||
// over partitions: functions that received its counts, functions whose
|
||||
// control flow changed since it was collected (hash mismatch), and functions
|
||||
// it has no record of (new, or never run in training: the merged profile is
|
||||
// sparse). A partition reused from the cache without its counts is counted
|
||||
// in Unknown.
|
||||
struct ProfileMatch {
|
||||
std::atomic<uint64_t> Annotated{0};
|
||||
std::atomic<uint64_t> Mismatched{0};
|
||||
std::atomic<uint64_t> Missing{0};
|
||||
std::atomic<uint64_t> Unknown{0};
|
||||
|
||||
void add(const ProfileMatch &Other) {
|
||||
Annotated += Other.Annotated;
|
||||
Mismatched += Other.Mismatched;
|
||||
Missing += Other.Missing;
|
||||
}
|
||||
|
||||
std::string serialize() const {
|
||||
return formatv("{0} {1} {2}", Annotated.load(), Mismatched.load(),
|
||||
Missing.load())
|
||||
.str();
|
||||
}
|
||||
|
||||
bool deserialize(StringRef Text) {
|
||||
SmallVector<StringRef, 3> Parts;
|
||||
Text.split(Parts, ' ');
|
||||
uint64_t Values[3];
|
||||
if (Parts.size() != 3)
|
||||
return false;
|
||||
for (size_t I = 0; I < 3; ++I)
|
||||
if (Parts[I].getAsInteger(10, Values[I]))
|
||||
return false;
|
||||
Annotated += Values[0];
|
||||
Mismatched += Values[1];
|
||||
Missing += Values[2];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Counts the profile annotation's hash-mismatch and missing-function
|
||||
// warnings instead of printing them. Every other diagnostic takes the
|
||||
// context's default handling.
|
||||
class ProfileMatchHandler : public DiagnosticHandler {
|
||||
public:
|
||||
explicit ProfileMatchHandler(ProfileMatch &Match) : Match(Match) {}
|
||||
|
||||
bool handleDiagnostics(const DiagnosticInfo &Info) override {
|
||||
if (Info.getKind() != DK_PGOProfile)
|
||||
return false;
|
||||
std::string Message;
|
||||
raw_string_ostream Stream(Message);
|
||||
DiagnosticPrinterRawOStream Printer(Stream);
|
||||
Info.print(Printer);
|
||||
if (StringRef(Message).contains("hash mismatch"))
|
||||
++Match.Mismatched;
|
||||
else if (StringRef(Message).contains_insensitive("no profile data"))
|
||||
++Match.Missing;
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
ProfileMatch &Match;
|
||||
};
|
||||
|
||||
// The annotation warns about functions the profile does not describe only
|
||||
// when asked; the match counts need those warnings. Process-wide, set once.
|
||||
void reportMissingProfiles() {
|
||||
static const bool Reported = [] {
|
||||
if (cl::Option *Option =
|
||||
cl::getRegisteredOptions().lookup("pgo-warn-missing-function"))
|
||||
Option->addOccurrence(0, "pgo-warn-missing-function", "true");
|
||||
return true;
|
||||
}();
|
||||
(void)Reported;
|
||||
}
|
||||
|
||||
// Functions of `M` that the profile annotated with counts.
|
||||
uint64_t annotatedFunctions(const Module &M) {
|
||||
uint64_t Count = 0;
|
||||
for (const Function &F : M) {
|
||||
if (F.isDeclaration())
|
||||
continue;
|
||||
std::optional<Function::ProfileCount> Entry = F.getEntryCount();
|
||||
if (Entry && !Entry->isSynthetic())
|
||||
++Count;
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
// Prints the profile match as the successful emit's one line of JSON.
|
||||
void reportProfileMatch(const ProfileMatch &Match) {
|
||||
json::Object Profile{{"annotated", Match.Annotated.load()},
|
||||
{"mismatched", Match.Mismatched.load()},
|
||||
{"missing", Match.Missing.load()},
|
||||
{"unknown_partitions", Match.Unknown.load()}};
|
||||
json::Object Response{{"ok", true}, {"profile", std::move(Profile)}};
|
||||
outs() << formatv("{0}\n", json::Value(std::move(Response)));
|
||||
}
|
||||
|
||||
// Reads the profile's header once, before any partition needs it: it must
|
||||
// hold IR-level counts.
|
||||
std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
|
||||
@@ -191,6 +296,7 @@ std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
|
||||
|
||||
// Owns the analysis managers a pipeline needs for one module.
|
||||
struct Pipeline {
|
||||
PassInstrumentationCallbacks Callbacks;
|
||||
LoopAnalysisManager LAM;
|
||||
FunctionAnalysisManager FAM;
|
||||
CGSCCAnalysisManager CGAM;
|
||||
@@ -199,13 +305,26 @@ struct Pipeline {
|
||||
|
||||
Pipeline(TargetMachine &Machine, OptimizationLevel Level,
|
||||
std::optional<PGOOptions> Profile = std::nullopt)
|
||||
: PB(&Machine, tuningFor(Level), std::move(Profile)) {
|
||||
: PB(&Machine, tuningFor(Level), std::move(Profile), &Callbacks) {
|
||||
PB.registerModuleAnalyses(MAM);
|
||||
PB.registerCGSCCAnalyses(CGAM);
|
||||
PB.registerFunctionAnalyses(FAM);
|
||||
PB.registerLoopAnalyses(LAM);
|
||||
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
|
||||
}
|
||||
|
||||
// Adds to Count the functions the profile annotation gave counts to, as
|
||||
// soon as that pass has run: inlining and dead code elimination later in
|
||||
// the pipeline delete annotated functions.
|
||||
void countAnnotated(std::atomic<uint64_t> &Count) {
|
||||
Callbacks.registerAfterPassCallback(
|
||||
[&Count](StringRef PassID, Any IR, const PreservedAnalyses &) {
|
||||
if (PassID != "PGOInstrumentationUse")
|
||||
return;
|
||||
if (const auto *M = llvm::any_cast<const Module *>(&IR))
|
||||
Count += annotatedFunctions(**M);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<EmitFailure> verify(Module &M, StringRef Code) {
|
||||
@@ -387,8 +506,15 @@ std::optional<EmitFailure> publish(StringRef TemporaryPath,
|
||||
std::optional<EmitFailure>
|
||||
emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level,
|
||||
std::optional<PGOOptions> Profile, CodeGenFileType Type,
|
||||
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries) {
|
||||
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries,
|
||||
ProfileMatch &Match) {
|
||||
bool Annotating = Profile && Profile->Action == PGOOptions::IRUse;
|
||||
if (Annotating)
|
||||
M.getContext().setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(Match));
|
||||
Pipeline P(Machine, Level, std::move(Profile));
|
||||
if (Annotating)
|
||||
P.countAnnotated(Match.Annotated);
|
||||
P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM);
|
||||
if (std::optional<EmitFailure> Failure =
|
||||
verify(M, "post_optimization_verification_failed"))
|
||||
@@ -555,20 +681,32 @@ std::optional<EmitFailure>
|
||||
compileOptimizedPartitions(const EmitOptions &Options,
|
||||
const PartitionPlan &Plan, OptimizationLevel Level,
|
||||
StringRef Salt, const ArtifactCache &Cache,
|
||||
std::vector<SmallVector<char, 0>> &Objects) {
|
||||
std::vector<SmallVector<char, 0>> &Objects,
|
||||
ProfileMatch &Match) {
|
||||
size_t Count = Plan.Modules.size();
|
||||
bool Annotating = !Options.ProfileUse.empty();
|
||||
std::vector<std::unique_ptr<MemoryBuffer>> Summaries(Count);
|
||||
SharedFailure Failures;
|
||||
forEachPartition(Count, [&](size_t I) {
|
||||
std::string Key = hashParts({Salt, "summary", Plan.Modules[I]});
|
||||
if ((Summaries[I] = Cache.lookup(Key, "bc")))
|
||||
if ((Summaries[I] = Cache.lookup(Key, "bc"))) {
|
||||
if (Annotating) {
|
||||
std::unique_ptr<MemoryBuffer> Counts = Cache.lookup(Key, "match");
|
||||
if (!Counts || !Match.deserialize(Counts->getBuffer()))
|
||||
++Match.Unknown;
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::string LookupError;
|
||||
std::unique_ptr<TargetMachine> Machine =
|
||||
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
|
||||
if (!Machine)
|
||||
return Failures.set({"target_machine_failed", LookupError});
|
||||
LLVMContext Context;
|
||||
ProfileMatch PartitionMatch;
|
||||
if (Annotating)
|
||||
Context.setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(PartitionMatch));
|
||||
Expected<std::unique_ptr<Module>> Loaded =
|
||||
loadPartition(Options, Plan.Modules[I], I, Context, *Machine);
|
||||
if (!Loaded)
|
||||
@@ -592,12 +730,19 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
// Profile instrumentation or annotation happens here, before the thin
|
||||
// link; the post-link optimization reads the annotated weights.
|
||||
Pipeline P(*Machine, Level, profileOptions(Options));
|
||||
if (Annotating)
|
||||
P.countAnnotated(PartitionMatch.Annotated);
|
||||
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
|
||||
MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false,
|
||||
/*EmitSummaryIndex=*/true,
|
||||
/*EmitModuleHash=*/true));
|
||||
MPM.run(Mod, P.MAM);
|
||||
}
|
||||
if (Annotating) {
|
||||
Match.add(PartitionMatch);
|
||||
// Stored before the summary, so a reused summary finds its counts.
|
||||
Cache.store(Key, "match", PartitionMatch.serialize());
|
||||
}
|
||||
StringRef Contents(Bitcode.data(), Bitcode.size());
|
||||
Cache.store(Key, "bc", Contents);
|
||||
Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents);
|
||||
@@ -744,7 +889,8 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
|
||||
std::optional<EmitFailure>
|
||||
compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
|
||||
std::vector<SmallVector<char, 0>> &Objects) {
|
||||
std::vector<SmallVector<char, 0>> &Objects,
|
||||
ProfileMatch &Match) {
|
||||
// Everything besides a partition's own bytes that determines its
|
||||
// artifacts. The caller's cache directory already identifies the exact
|
||||
// helper build; source content and partition count arrive per module.
|
||||
@@ -778,7 +924,7 @@ compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
|
||||
return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects);
|
||||
return compileOptimizedPartitions(Options, Plan,
|
||||
optimizationLevel(Options.OptLevel), Salt,
|
||||
Cache, Objects);
|
||||
Cache, Objects, Match);
|
||||
}
|
||||
|
||||
// Publishes in-memory partition objects through private sibling files.
|
||||
@@ -819,6 +965,8 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
std::vector<SmallVector<char, 0>> Objects;
|
||||
std::optional<EmitFailure> Failure;
|
||||
bool Compiled = false;
|
||||
// A failed attempt's counts are discarded with its objects.
|
||||
auto Match = std::make_unique<ProfileMatch>();
|
||||
{
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> Input =
|
||||
MemoryBuffer::getFile(Options.Input, /*IsText=*/false,
|
||||
@@ -830,15 +978,16 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
Input->reset();
|
||||
PartitionPlan Plan{std::move(Text->Sources),
|
||||
std::move(Text->PublicSymbols)};
|
||||
Compiled = !compilePartitions(Options, Plan, Objects);
|
||||
Compiled = !compilePartitions(Options, Plan, Objects, *Match);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Compiled) {
|
||||
Match = std::make_unique<ProfileMatch>();
|
||||
PartitionPlan Plan;
|
||||
Failure = splitParsedModule(Options, Plan);
|
||||
if (!Failure)
|
||||
Failure = compilePartitions(Options, Plan, Objects);
|
||||
Failure = compilePartitions(Options, Plan, Objects, *Match);
|
||||
}
|
||||
std::vector<SmallString<256>> Temporaries;
|
||||
if (!Failure)
|
||||
@@ -851,6 +1000,8 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(*Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -895,6 +1046,8 @@ int emit(const EmitOptions &Options) {
|
||||
return reportError("invalid_import",
|
||||
"runtime bitcode import requires an optimized build",
|
||||
Options.DiagnosticFormat);
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportMissingProfiles();
|
||||
if (std::optional<EmitFailure> Failure = inspectProfile(Options))
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
@@ -957,9 +1110,10 @@ int emit(const EmitOptions &Options) {
|
||||
? CodeGenFileType::ObjectFile
|
||||
: CodeGenFileType::AssemblyFile;
|
||||
std::vector<SmallString<256>> Temporaries;
|
||||
ProfileMatch Match;
|
||||
std::optional<EmitFailure> Failure =
|
||||
emitModule(*Mod, *Machine, Level, profileOptions(Options), Type,
|
||||
Options.Outputs[0], Temporaries);
|
||||
Options.Outputs[0], Temporaries, Match);
|
||||
if (!Failure)
|
||||
Failure = publish(Temporaries[0], Options.Outputs[0]);
|
||||
if (Failure) {
|
||||
@@ -968,6 +1122,8 @@ int emit(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
@@ -36,6 +37,9 @@ struct Symbol {
|
||||
std::string PromotedHeader;
|
||||
std::vector<unsigned> Refs;
|
||||
std::vector<unsigned> Metadata;
|
||||
// The source-module group the emitter wrote before this definition, or
|
||||
// empty.
|
||||
StringRef Group;
|
||||
unsigned Partition = NoPartition;
|
||||
};
|
||||
|
||||
@@ -70,6 +74,83 @@ uint32_t stableHash(StringRef Name) {
|
||||
return Hash;
|
||||
}
|
||||
|
||||
// Places the functions whose root carries a source-module group by group: a
|
||||
// module's functions call each other most, so keeping them together leaves
|
||||
// fewer calls for the thin link to import across partitions. A group larger
|
||||
// than the mean partition, or than MaxChunkBytes, is divided into a power of
|
||||
// two of chunks by the hash of each root's name, so the division changes only
|
||||
// when the group's size doubles. Chunks are placed largest first, each in the
|
||||
// lighter of two partitions chosen by hashes of its key, after the functions
|
||||
// placed by name. The placement depends only on the module text and the
|
||||
// partition count. Modules with fewer than MinGroupedPartitions partitions
|
||||
// keep the placement by name: with few partitions per core, the balance of
|
||||
// the largest partitions decides the build time more than the imports.
|
||||
void placeGroups(std::vector<Symbol> &Symbols, ArrayRef<unsigned> Roots,
|
||||
unsigned Count) {
|
||||
constexpr unsigned MinGroupedPartitions = 32;
|
||||
if (Count < MinGroupedPartitions)
|
||||
return;
|
||||
auto Bytes = [](const Symbol &S) -> uint64_t {
|
||||
return S.Header.size() + S.Body.size();
|
||||
};
|
||||
std::vector<uint64_t> Load(Count, 0);
|
||||
uint64_t Total = 0;
|
||||
StringMap<uint64_t> GroupBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
Total += Bytes(S);
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (Group.empty())
|
||||
Load[S.Partition] += Bytes(S);
|
||||
else
|
||||
GroupBytes[Group] += Bytes(S);
|
||||
}
|
||||
if (GroupBytes.empty())
|
||||
return;
|
||||
// A chunk is at most MaxChunkBytes, so the code a partition optimizes
|
||||
// together, and the inlining within it, does not grow when a program has
|
||||
// fewer partitions.
|
||||
constexpr uint64_t MaxChunkBytes = 1024 * 1024;
|
||||
uint64_t Chunk =
|
||||
std::min(MaxChunkBytes, std::max<uint64_t>(1, Total / Count));
|
||||
std::vector<std::string> Keys(Symbols.size());
|
||||
StringMap<uint64_t> KeyBytes;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (S.Kind != SymbolKind::Function || Group.empty())
|
||||
continue;
|
||||
uint64_t Chunks = 1;
|
||||
while (Chunks * Chunk < GroupBytes[Group])
|
||||
Chunks *= 2;
|
||||
Keys[I] = Chunks == 1
|
||||
? Group.str()
|
||||
: (Group + "#" +
|
||||
Twine(stableHash(Symbols[Roots[I]].Name) % Chunks))
|
||||
.str();
|
||||
KeyBytes[Keys[I]] += Bytes(S);
|
||||
}
|
||||
std::vector<std::pair<StringRef, uint64_t>> Order;
|
||||
for (const auto &Entry : KeyBytes)
|
||||
Order.push_back({Entry.getKey(), Entry.getValue()});
|
||||
llvm::sort(Order, [](const auto &A, const auto &B) {
|
||||
return A.second != B.second ? A.second > B.second : A.first < B.first;
|
||||
});
|
||||
StringMap<unsigned> Where;
|
||||
for (const auto &[Key, Size] : Order) {
|
||||
unsigned First = stableHash(Key) % Count;
|
||||
unsigned Second = stableHash((Key + "/2").str()) % Count;
|
||||
unsigned Chosen = Load[Second] < Load[First] ? Second : First;
|
||||
Load[Chosen] += Size;
|
||||
Where[Key] = Chosen;
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (!Keys[I].empty())
|
||||
Symbols[I].Partition = Where[Keys[I]];
|
||||
}
|
||||
|
||||
bool startsWithLinkage(StringRef Rest) {
|
||||
for (StringRef Linkage :
|
||||
{"linkonce", "weak", "common", "appending", "extern_weak",
|
||||
@@ -326,6 +407,11 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
return true;
|
||||
};
|
||||
|
||||
// The emitter writes "; scriptc-group <key>" before the definitions of each
|
||||
// source function; the group applies until the next such line, and an
|
||||
// empty key ends it.
|
||||
constexpr StringRef GroupLine = "; scriptc-group";
|
||||
StringRef Group;
|
||||
size_t Pos = 0;
|
||||
while (Pos < Text.size()) {
|
||||
size_t End = Text.find('\n', Pos);
|
||||
@@ -335,10 +421,15 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
size_t Next = End + 1;
|
||||
if (Line.empty() || Line.trim().empty()) {
|
||||
// Blank separator.
|
||||
} else if (Line.starts_with(GroupLine) &&
|
||||
(Line.size() == GroupLine.size() ||
|
||||
Line[GroupLine.size()] == ' ')) {
|
||||
Group = Line.drop_front(GroupLine.size()).trim();
|
||||
} else if (Line.starts_with("define ")) {
|
||||
Symbol S;
|
||||
S.Kind = SymbolKind::Function;
|
||||
S.Header = Line;
|
||||
S.Group = Group;
|
||||
if (!parseFunctionHeader(Line, S))
|
||||
return std::nullopt;
|
||||
// The emitter indents every body line, so the first line that is
|
||||
@@ -466,6 +557,7 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
Referrer[Ref] = I;
|
||||
}
|
||||
}
|
||||
std::vector<unsigned> Roots(Symbols.size());
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind != SymbolKind::Function)
|
||||
continue;
|
||||
@@ -479,8 +571,10 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
break;
|
||||
Root = Parent;
|
||||
}
|
||||
Roots[I] = Root;
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
}
|
||||
placeGroups(Symbols, Roots, Count);
|
||||
for (const Symbol &S : Symbols) {
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
|
||||
@@ -20,11 +20,15 @@ struct ProgramPartitions {
|
||||
};
|
||||
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
// it, so the partitions can be parsed concurrently. In modules of 32 or
|
||||
// more partitions, functions under a "; scriptc-group <key>" line are placed
|
||||
// with the other functions of that source-module group, which is divided
|
||||
// into chunks when it is larger than the mean partition or 1 MiB; other
|
||||
// functions are placed by a stable hash of their name. An internal function referenced by exactly one other function stays
|
||||
// with that function unless its body is too large to inline, and a global
|
||||
// stays with the first function that references it. Returns nullopt for any
|
||||
// construct outside the emitter's narrow top-level grammar; callers then
|
||||
// split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -72,7 +72,9 @@ async function tryFastPath(): Promise<number | null> {
|
||||
values.sparse ||
|
||||
// The full compiler resolves `native` against the build host.
|
||||
values["target-cpu"] !== undefined ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The full CLI validates the Node island policy flags.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
const backend = values.backend;
|
||||
@@ -182,6 +184,21 @@ async function tryFastPath(): Promise<number | null> {
|
||||
});
|
||||
}
|
||||
|
||||
function hasNodeIslandFlags(
|
||||
values: ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
>["values"],
|
||||
): boolean {
|
||||
return (
|
||||
values["node-islands"] !== undefined ||
|
||||
(values["node-island"] ?? []).length > 0 ||
|
||||
(values.native ?? []).length > 0 ||
|
||||
values.node !== undefined ||
|
||||
values["explain-island"] !== undefined ||
|
||||
values["strict-user-js"]
|
||||
);
|
||||
}
|
||||
|
||||
/** An unchanged program's coverage verdict replays from the verdict cache
|
||||
* without loading the compiler graph. Anything else (a miss, a comment-only
|
||||
* edit, options the replay does not cover) defers to the full CLI. */
|
||||
@@ -212,7 +229,9 @@ async function coverageFastPath(
|
||||
values["provenance-sources"] ||
|
||||
values["emit-ir"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The replay covers the default Node island policy only.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
let startup: typeof import("@scriptc/compiler/startup-cache");
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v37");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v44");
|
||||
});
|
||||
});
|
||||
|
||||
+184
-39
@@ -2796,8 +2796,9 @@ declare var URLSearchParams: {
|
||||
* slashes, non-empty host); pathToFileURL resolves the path and percent-
|
||||
* encodes it into a file: URL. The legacy API: parse() answers a Url
|
||||
* object, format() serializes a Url-shaped object, a string, or a URL,
|
||||
* resolve() resolves one URL string against another, and the domain pair
|
||||
* converts a hostname to and from its Punycode form. */
|
||||
* resolve() resolves one URL string against another, the domain pair
|
||||
* converts a hostname to and from its Punycode form, and urlToHttpOptions
|
||||
* builds http.request options from a URL. */
|
||||
declare module "url" {
|
||||
export type URL = globalThis.URL;
|
||||
export type URLSearchParams = globalThis.URLSearchParams;
|
||||
@@ -2865,6 +2866,19 @@ declare module "url" {
|
||||
export function resolve(from: string, to: string): string;
|
||||
export function domainToASCII(domain: string): string;
|
||||
export function domainToUnicode(domain: string): string;
|
||||
/** urlToHttpOptions's null-prototype request options. */
|
||||
export interface UrlHttpOptions {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
hash: string;
|
||||
search: string;
|
||||
pathname: string;
|
||||
path: string;
|
||||
href: string;
|
||||
port?: number;
|
||||
auth?: string;
|
||||
}
|
||||
export function urlToHttpOptions(url: URL): UrlHttpOptions;
|
||||
}
|
||||
declare module "node:url" {
|
||||
export * from "url";
|
||||
@@ -2900,6 +2914,8 @@ declare module "crypto" {
|
||||
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
|
||||
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
|
||||
format: T;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface AsymmetricKeyDetails {
|
||||
modulusLength?: number;
|
||||
@@ -2924,6 +2940,7 @@ declare module "crypto" {
|
||||
format?: "pem" | "der" | undefined;
|
||||
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
|
||||
encoding?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface PublicKeyInput {
|
||||
key: string | Buffer;
|
||||
@@ -2960,12 +2977,22 @@ declare module "crypto" {
|
||||
modulusLength: number;
|
||||
publicExponent?: number | undefined;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
namedCurve: string;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "sec1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "rsa",
|
||||
@@ -2993,7 +3020,12 @@ declare module "crypto" {
|
||||
): KeyPairKeyObjectResult;
|
||||
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "ed25519",
|
||||
@@ -3006,7 +3038,12 @@ declare module "crypto" {
|
||||
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
|
||||
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "x25519",
|
||||
@@ -3537,17 +3574,45 @@ declare module "crypto" {
|
||||
keylen: number,
|
||||
callback: (error: Error | null, derivedKey: ArrayBuffer) => void,
|
||||
): void;
|
||||
/* The lowered X509Certificate surface is the data-record slice:
|
||||
* fingerprint (the SHA-1 of the DER, uppercase colon-separated) and
|
||||
* the validFrom/validTo validity window (Node's ASN1_TIME_print
|
||||
* strings — "Jul 1 00:00:00 2026 GMT"; the cert-expiry idiom composes
|
||||
* them with new Date(...).getTime()). All compute at construction;
|
||||
* unparseable input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
|
||||
/* The lowered X509Certificate surface is the data-record slice: the
|
||||
* fields below, all computed at construction (raw decodes the input,
|
||||
* and the others read raw). validFrom/validTo are Node's
|
||||
* ASN1_TIME_print strings ("Jul 1 00:00:00 2026 GMT"). Unparseable
|
||||
* input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
|
||||
export class X509Certificate {
|
||||
constructor(buffer: Uint8Array | string);
|
||||
readonly ca: boolean;
|
||||
readonly fingerprint: string;
|
||||
readonly fingerprint256: string;
|
||||
readonly fingerprint512: string;
|
||||
readonly subject: string;
|
||||
readonly subjectAltName: string | undefined;
|
||||
readonly infoAccess: string | undefined;
|
||||
readonly issuer: string;
|
||||
readonly raw: Buffer;
|
||||
readonly serialNumber: string;
|
||||
readonly signatureAlgorithm: string | undefined;
|
||||
readonly signatureAlgorithmOid: string;
|
||||
readonly validFrom: string;
|
||||
readonly validFromDate: Date;
|
||||
readonly validTo: string;
|
||||
readonly validToDate: Date;
|
||||
/* the certificate's public key; every read answers the same KeyObject */
|
||||
readonly publicKey: KeyObject;
|
||||
verify(publicKey: KeyObject): boolean;
|
||||
checkPrivateKey(privateKey: KeyObject): boolean;
|
||||
checkIssued(otherCert: X509Certificate): boolean;
|
||||
checkHost(name: string, options?: X509CheckOptions): string | undefined;
|
||||
checkEmail(email: string, options?: X509CheckOptions): string | undefined;
|
||||
checkIP(ip: string): string | undefined;
|
||||
toLegacyObject(): import("tls").PeerCertificate;
|
||||
}
|
||||
export interface X509CheckOptions {
|
||||
subject?: "always" | "default" | "never";
|
||||
wildcards?: boolean;
|
||||
partialWildcards?: boolean;
|
||||
multiLabelWildcards?: boolean;
|
||||
singleLabelSubdomains?: boolean;
|
||||
}
|
||||
/* The crypto introspection statics: getFips() answers 0 (a compiled
|
||||
* binary is never a FIPS build), the three name lists bake as fresh
|
||||
@@ -4858,9 +4923,9 @@ declare module "node:readline/promises" {
|
||||
* inputs with Node's default options; libz links only into zlib-using
|
||||
* binaries. Callback forms use the executable worker pool. Options stay
|
||||
* declared so reached uses receive a named SC2020 refusal. The Deflate,
|
||||
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are
|
||||
* runtime Transform streams; Brotli and Zstd remain outside the lowered
|
||||
* surface. */
|
||||
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, Unzip, BrotliCompress
|
||||
* and BrotliDecompress streams are runtime Transform streams; Zstd remains
|
||||
* outside the lowered surface. */
|
||||
declare module "zlib" {
|
||||
import { Transform } from "stream";
|
||||
export interface ZlibOptions {
|
||||
@@ -4917,50 +4982,66 @@ declare module "zlib" {
|
||||
): void;
|
||||
export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer;
|
||||
export function crc32(data: string | Uint8Array, value?: number): number;
|
||||
export function brotliCompressSync(data: string | Uint8Array): Buffer;
|
||||
export function brotliDecompressSync(data: Uint8Array): Buffer;
|
||||
/* Brotli needs a codec the static runtime does not vendor: these are
|
||||
* declared so their uses receive the named refusal. */
|
||||
/* Brotli over the vendored brotli 1.2.0 (scr_brotli.c): `params` keys
|
||||
* are the numeric BROTLI_PARAM_* and BROTLI_DECODER_PARAM_* constants. */
|
||||
export interface BrotliOptions {
|
||||
[option: string]: unknown;
|
||||
}
|
||||
export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void;
|
||||
export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void;
|
||||
export function createBrotliCompress(options?: ZlibOptions): Transform;
|
||||
export function createBrotliDecompress(options?: ZlibOptions): Transform;
|
||||
export class BrotliCompress extends Transform {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class BrotliDecompress extends Transform {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export function brotliCompress(
|
||||
data: string | Uint8Array,
|
||||
options: BrotliOptions,
|
||||
callback: ZlibCallback,
|
||||
): void;
|
||||
export function brotliCompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
|
||||
export function brotliDecompress(data: string | Uint8Array, callback: ZlibCallback): void;
|
||||
export function brotliDecompress(
|
||||
data: string | Uint8Array,
|
||||
options: BrotliOptions,
|
||||
callback: ZlibCallback,
|
||||
): void;
|
||||
export function brotliDecompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
|
||||
/* The stream classes: runtime Transform streams over the zlib engine
|
||||
* (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build
|
||||
* the same stream; options are validated at runtime with Node's rules. */
|
||||
* the same stream; options are validated at runtime with Node's rules.
|
||||
* Brotli streams have no params(), as in Node. */
|
||||
class ZlibBase extends Transform {
|
||||
readonly bytesWritten: number;
|
||||
flush(kind?: number, callback?: () => void): void;
|
||||
flush(callback: () => void): void;
|
||||
params(level: number, strategy: number, callback?: () => void): void;
|
||||
close(callback?: () => void): void;
|
||||
reset(): void;
|
||||
}
|
||||
export class Deflate extends ZlibBase {
|
||||
class Zlib extends ZlibBase {
|
||||
params(level: number, strategy: number, callback?: () => void): void;
|
||||
}
|
||||
export class BrotliCompress extends ZlibBase {
|
||||
constructor(options?: BrotliOptions);
|
||||
}
|
||||
export class BrotliDecompress extends ZlibBase {
|
||||
constructor(options?: BrotliOptions);
|
||||
}
|
||||
export function createBrotliCompress(options?: BrotliOptions): BrotliCompress;
|
||||
export function createBrotliDecompress(options?: BrotliOptions): BrotliDecompress;
|
||||
export class Deflate extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Inflate extends ZlibBase {
|
||||
export class Inflate extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Gzip extends ZlibBase {
|
||||
export class Gzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Gunzip extends ZlibBase {
|
||||
export class Gunzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class DeflateRaw extends ZlibBase {
|
||||
export class DeflateRaw extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class InflateRaw extends ZlibBase {
|
||||
export class InflateRaw extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export class Unzip extends ZlibBase {
|
||||
export class Unzip extends Zlib {
|
||||
constructor(options?: ZlibOptions);
|
||||
}
|
||||
export function createDeflate(options?: ZlibOptions): Deflate;
|
||||
@@ -5443,6 +5524,11 @@ declare module "net" {
|
||||
/* The demux route: hand an accepted socket to another server. */
|
||||
emit(event: "connection", socket: Socket): boolean;
|
||||
on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
on(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
on(event: "close" | "listening", listener: () => void): void;
|
||||
on(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
on(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5462,6 +5548,11 @@ declare module "net" {
|
||||
): void;
|
||||
/* addListener IS on (Node aliases them) — the suite spells both. */
|
||||
addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
addListener(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
addListener(event: "close" | "listening", listener: () => void): void;
|
||||
addListener(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
addListener(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5475,6 +5566,11 @@ declare module "net" {
|
||||
listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void,
|
||||
): void;
|
||||
once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
once(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
once(event: "close" | "listening", listener: () => void): void;
|
||||
once(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
once(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5928,8 +6024,15 @@ declare module "node:http" {
|
||||
* are PEM strings or Buffers. Handshake failures tear the socket down
|
||||
* without emitting an event, Node's default for 'tlsClientError' with no listener. */
|
||||
declare module "tls" {
|
||||
import { Server, Socket } from "net";
|
||||
export { Server };
|
||||
import { Server as NetServer, Socket } from "net";
|
||||
/** tls.Server: a net server whose connections handshake first. */
|
||||
export interface Server extends NetServer {
|
||||
/* serve `context` to clients whose SNI name matches `hostname` ('*'
|
||||
* matches within one label); the newest match wins */
|
||||
addContext(hostname: string, context: TlsOptions | SecureContext): void;
|
||||
/* new connections serve a context made from `options` */
|
||||
setSecureContext(options: TlsOptions): void;
|
||||
}
|
||||
export interface TlsOptions {
|
||||
/* Optional in the DECLARATION only (real Node programs build these
|
||||
* literals with every TLS knob): the lowering still requires both —
|
||||
@@ -5963,7 +6066,47 @@ declare module "tls" {
|
||||
getCipher(): CipherNameAndProtocol;
|
||||
/* whether the handshake resumed a cached session */
|
||||
isSessionReused(): boolean;
|
||||
/* the peer's certificate: {} before the handshake or without one,
|
||||
* null once the socket is destroyed; detailed adds issuerCertificate */
|
||||
getPeerCertificate(detailed: true): DetailedPeerCertificate;
|
||||
getPeerCertificate(detailed?: false): PeerCertificate;
|
||||
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate;
|
||||
/* the certificate this socket serves, the same way */
|
||||
getCertificate(): PeerCertificate | object | null;
|
||||
/* a new X509Certificate on each call, or undefined without one */
|
||||
getPeerX509Certificate(): import("crypto").X509Certificate | undefined;
|
||||
getX509Certificate(): import("crypto").X509Certificate | undefined;
|
||||
}
|
||||
/* A certificate as getPeerCertificate() and X509Certificate's
|
||||
* toLegacyObject() answer it: subject and issuer map attribute short
|
||||
* names to values, infoAccess its "<method> - <type>" keys to values. */
|
||||
export interface PeerCertificate {
|
||||
subject: { [attribute: string]: string | string[] };
|
||||
issuer: { [attribute: string]: string | string[] };
|
||||
subjectaltname?: string;
|
||||
infoAccess?: { [key: string]: string[] };
|
||||
ca: boolean;
|
||||
modulus?: string;
|
||||
exponent?: string;
|
||||
pubkey?: Buffer;
|
||||
bits?: number;
|
||||
valid_from: string;
|
||||
valid_to: string;
|
||||
fingerprint: string;
|
||||
fingerprint256: string;
|
||||
fingerprint512: string;
|
||||
ext_key_usage?: string[];
|
||||
serialNumber: string;
|
||||
raw: Buffer;
|
||||
asn1Curve?: string;
|
||||
nistCurve?: string;
|
||||
}
|
||||
/* getPeerCertificate(true): each certificate links to its issuer; a
|
||||
* self-issued root is its own issuerCertificate */
|
||||
export interface DetailedPeerCertificate extends PeerCertificate {
|
||||
issuerCertificate: DetailedPeerCertificate;
|
||||
}
|
||||
|
||||
/** new tls.TLSSocket(socket, options): the server-mode wrap of an open
|
||||
* socket (a server connection or an http CONNECT socket). The options
|
||||
* record reads at runtime; isServer, cert, key, secureContext,
|
||||
@@ -6018,6 +6161,8 @@ declare module "tls" {
|
||||
/** An opaque parsed cert/key pair — what an SNI callback answers with
|
||||
* (the value is a runtime handle; the members are runtime-internal). */
|
||||
export interface SecureContext {}
|
||||
/* @types/node's name for createSecureContext's options */
|
||||
export type SecureContextOptions = TlsOptions;
|
||||
export function createSecureContext(options: TlsOptions): SecureContext;
|
||||
/** The CA-store introspection surface (scr_tls_ca.c): per-type cached
|
||||
* PEM string arrays. The host bundle stands in for Node's compiled-in
|
||||
|
||||
@@ -70,3 +70,24 @@ interface PromiseConstructor {
|
||||
reject: (reason: Error) => void;
|
||||
};
|
||||
}
|
||||
|
||||
/* Carriers of the types the lowerer gives untyped JavaScript collections
|
||||
* whose uses prove them (js-collection-types.ts). Nothing reads them at run
|
||||
* time. */
|
||||
declare namespace __scriptcInferredCollections {
|
||||
const map_string_string: Map<string, string>;
|
||||
const map_string_number: Map<string, number>;
|
||||
const map_string_boolean: Map<string, boolean>;
|
||||
const map_number_string: Map<number, string>;
|
||||
const map_number_number: Map<number, number>;
|
||||
const map_number_boolean: Map<number, boolean>;
|
||||
const map_boolean_string: Map<boolean, string>;
|
||||
const map_boolean_number: Map<boolean, number>;
|
||||
const map_boolean_boolean: Map<boolean, boolean>;
|
||||
const set_string: Set<string>;
|
||||
const set_number: Set<number>;
|
||||
const set_boolean: Set<boolean>;
|
||||
const array_string: string[];
|
||||
const array_number: number[];
|
||||
const array_boolean: boolean[];
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -135,3 +135,16 @@ void scr_cyc_set_growth_cap(long cap);
|
||||
void scriptc_native_bound_cycle_growth(void) {
|
||||
scr_cyc_set_growth_cap(2);
|
||||
}
|
||||
|
||||
/* The runtime's nursery trigger (scr_rc.h). */
|
||||
void scr_cyc_set_nursery_threshold(long threshold);
|
||||
|
||||
/* Runs nursery passes every 1024 new candidates instead of every 256. Much
|
||||
* of a build's cyclic garbage outlives 256 releases: in the shorter window
|
||||
* it survives a nursery pass, is promoted, and only full passes over the
|
||||
* whole live heap find it. A constructor sets it before the compiler's
|
||||
* modules initialize, so every pass of a build uses it.
|
||||
* SCR_CYCLE_THRESHOLD still overrides it. */
|
||||
__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) {
|
||||
scr_cyc_set_nursery_threshold(1024);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Reads cases from standard input, each a little-endian u32 length, a u32
|
||||
* start and the bytes, and prints scriptc_ts7_json_value_end for each. */
|
||||
#include "ts7-process.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
static int read_u32(uint32_t *value) {
|
||||
unsigned char bytes[4];
|
||||
if (fread(bytes, 1, 4, stdin) != 4) return 0;
|
||||
*value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 |
|
||||
(uint32_t)bytes[3] << 24;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
#ifdef _WIN32
|
||||
/* The cases are bytes: text mode would rewrite line endings. */
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
#endif
|
||||
uint32_t length, start;
|
||||
while (read_u32(&length)) {
|
||||
if (!read_u32(&start)) return 1;
|
||||
uint8_t *bytes = malloc(length == 0 ? 1 : length);
|
||||
if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1;
|
||||
printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start));
|
||||
free(bytes);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -12,7 +12,49 @@
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In
|
||||
* mode "listen-exit" it exits as an executable without --listen does. In
|
||||
* mode "listen" it answers each client, in order, with its client number,
|
||||
* its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the
|
||||
* client closes, and exits after SECONDS (at most 5) without a client. */
|
||||
static int listen_main(const char *mode, char **argv) {
|
||||
if (strcmp(mode, "listen-exit") == 0) return 2;
|
||||
if (strcmp(mode, "listen") != 0) return 73;
|
||||
struct sockaddr_un address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sun_family = AF_UNIX;
|
||||
if (strlen(argv[0]) >= sizeof(address.sun_path)) return 77;
|
||||
strcpy(address.sun_path, argv[0]);
|
||||
int listener = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(listener, 4) != 0)
|
||||
return 78;
|
||||
const char *memory = getenv("TSC_TS_LISTEN_MEMORY");
|
||||
int idle = atoi(argv[2]);
|
||||
if (idle > 5) idle = 5;
|
||||
for (int client = 1;; client++) {
|
||||
struct pollfd entry = {listener, POLLIN, 0};
|
||||
if (poll(&entry, 1, idle * 1000) <= 0) break;
|
||||
int fd = accept(listener, NULL, NULL);
|
||||
if (fd < 0) return 79;
|
||||
char line[512];
|
||||
int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2],
|
||||
memory == NULL ? "unset" : memory);
|
||||
if (size <= 0 || write(fd, line, (size_t)size) != size) return 80;
|
||||
uint8_t bytes[17];
|
||||
ssize_t count;
|
||||
while ((count = read(fd, bytes, sizeof(bytes))) > 0)
|
||||
if (write(fd, bytes, (size_t)count) != count) break;
|
||||
close(fd);
|
||||
}
|
||||
unlink(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int child_main(int argc, char **argv) {
|
||||
@@ -20,15 +62,25 @@ static int child_main(int argc, char **argv) {
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
_setmode(_fileno(stdout), _O_BINARY);
|
||||
#endif
|
||||
if (argc < 5 || argc > 6 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == 6 && strcmp(argv[5], "--timing") != 0)) return 71;
|
||||
int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0;
|
||||
int fixed = listening ? 9 : 5;
|
||||
int at = fixed, timing = 0, libraries = 0;
|
||||
if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; }
|
||||
if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; }
|
||||
if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0)
|
||||
return 71;
|
||||
const char *mode = argv[3];
|
||||
#ifndef _WIN32
|
||||
if (listening) return listen_main(mode, argv + 6);
|
||||
#endif
|
||||
if (strncmp(mode, "argv:", 5) == 0) {
|
||||
fwrite(mode + 5, 1, strlen(mode + 5), stdout);
|
||||
fputc('\n', stdout);
|
||||
fputs(argv[4], stdout);
|
||||
fputc('\n', stdout);
|
||||
fputs(argc == 6 ? "timing\n" : "plain\n", stdout);
|
||||
fputs(timing ? "timing\n" : "plain\n", stdout);
|
||||
if (libraries) fputs("libraries from disk\n", stdout);
|
||||
return ferror(stdout) ? 72 : 0;
|
||||
}
|
||||
if (strcmp(mode, "exit") == 0) return 0;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
/* flock(2), for the checker server's lock file, and syscall(2). */
|
||||
#define _DEFAULT_SOURCE
|
||||
#define _DARWIN_C_SOURCE
|
||||
#endif
|
||||
#include "ts7-process.h"
|
||||
#include <errno.h>
|
||||
@@ -16,7 +19,13 @@
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
@@ -38,6 +47,9 @@ typedef struct Ts7Process {
|
||||
HANDLE output;
|
||||
#else
|
||||
int socket;
|
||||
/* The checker server's lock file, held while connected to the server
|
||||
* (scriptc_ts7_attach); -1 for a server this process started. */
|
||||
int lock;
|
||||
#endif
|
||||
} Ts7Process;
|
||||
|
||||
@@ -149,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) {
|
||||
}
|
||||
|
||||
static int start_process(Ts7Process *p, char **args, size_t argc) {
|
||||
wchar_t *wide[6] = {0};
|
||||
wchar_t *wide[7] = {0};
|
||||
wchar_t *line = NULL;
|
||||
HANDLE child_input = NULL, child_output = NULL, child_error = NULL;
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
|
||||
@@ -283,7 +295,7 @@ static int start_process(Ts7Process *p, char **args, size_t argc) {
|
||||
if (error == 0) error = posix_spawn(&pid, args[0], &actions, NULL, args, environment);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
if (error == 0) {
|
||||
p->socket = sockets[0]; p->pid = (uint32_t)pid;
|
||||
p->socket = sockets[0]; p->pid = (uint32_t)pid; p->lock = -1;
|
||||
}
|
||||
done:
|
||||
close(sockets[1]);
|
||||
@@ -293,15 +305,21 @@ done:
|
||||
|
||||
static void stop_process(Ts7Process *p) {
|
||||
close(p->socket);
|
||||
// A shared checker server keeps running; closing the connection ends this
|
||||
// build's session, and closing the lock file lets another build connect.
|
||||
if (p->lock >= 0) { close(p->lock); return; }
|
||||
int status;
|
||||
pid_t pid = (pid_t)p->pid;
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
// A server exits within a fraction of a millisecond of reading EOF, so the
|
||||
// checks start 0.1 ms apart and double: 51.1 ms in all before the kill.
|
||||
for (int attempt = 0; attempt < 10; attempt++) {
|
||||
pid_t result;
|
||||
do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR);
|
||||
// ECHILD also means the PID no longer belongs to us. Never signal a
|
||||
// potentially recycled PID after somebody else has reaped the child.
|
||||
if (result == pid || (result < 0 && errno == ECHILD)) return;
|
||||
struct timespec delay = {0, 10000000};
|
||||
if (attempt == 9) break;
|
||||
struct timespec delay = {0, 100000L << attempt};
|
||||
while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {}
|
||||
}
|
||||
(void)kill(pid, SIGKILL);
|
||||
@@ -312,7 +330,7 @@ static void stop_process(Ts7Process *p) {
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing) {
|
||||
uint8_t timing, uint8_t libraries_from_disk) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *exe = copy_argument(executable, executable_size, "");
|
||||
if (exe == NULL) return 0;
|
||||
@@ -328,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
}
|
||||
exit_registered = 1;
|
||||
}
|
||||
char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL};
|
||||
int started = start_process(p, args, timing ? 6 : 5);
|
||||
char *args[8] = {exe, "--api", "--cwd", directory, callback_flag};
|
||||
size_t argc = 5;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
int started = start_process(p, args, argc);
|
||||
free(exe); free(directory); free(callback_flag);
|
||||
if (!started) { free(p); return 0; }
|
||||
p->id = next_id++;
|
||||
@@ -338,6 +360,184 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
return p->id;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory;
|
||||
error_text("a shared checker server needs Unix sockets");
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
/* This user's checker server directory: $XDG_RUNTIME_DIR/scriptc, or
|
||||
* /tmp/scriptc-<uid>. It must be a directory this user owns with no access
|
||||
* for others; one that exists otherwise is not used. */
|
||||
static int server_directory(char *path, size_t size) {
|
||||
const char *runtime = getenv("XDG_RUNTIME_DIR");
|
||||
int count = runtime != NULL && runtime[0] == '/'
|
||||
? snprintf(path, size, "%s/scriptc", runtime)
|
||||
: snprintf(path, size, "/tmp/scriptc-%lu", (unsigned long)geteuid());
|
||||
if (count < 0 || (size_t)count >= size) { error_text("checker server directory path is too long"); return 0; }
|
||||
if (mkdir(path, 0700) != 0 && errno != EEXIST) { system_error(errno); return 0; }
|
||||
struct stat info;
|
||||
if (lstat(path, &info) != 0) { system_error(errno); return 0; }
|
||||
if (!S_ISDIR(info.st_mode) || info.st_uid != geteuid() || (info.st_mode & 077) != 0) {
|
||||
error_text("checker server directory is not private to this user");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* A connection to the server listening at `path`, or -1. */
|
||||
static int connect_server(const char *path) {
|
||||
struct sockaddr_un address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sun_family = AF_UNIX;
|
||||
memcpy(address.sun_path, path, strlen(path) + 1);
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { close(fd); return -1; }
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int no_sigpipe = 1;
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)) != 0) { close(fd); return -1; }
|
||||
#endif
|
||||
int status;
|
||||
do { status = connect(fd, (const struct sockaddr *)&address, sizeof(address)); } while (status != 0 && errno == EINTR);
|
||||
if (status != 0) { close(fd); return -1; }
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Starts the server in its own process group, so that a terminal's signals
|
||||
* to the build do not reach it, with its standard streams on /dev/null and
|
||||
* no other descriptor of this process, so that it holds no pipe a caller of
|
||||
* the build waits on. `limit` (TSC_TS_LISTEN_MEMORY=...) is added to the
|
||||
* environment unless it sets TSC_TS_LISTEN_MEMORY. */
|
||||
static pid_t start_server(char **args, char *limit) {
|
||||
#ifdef __APPLE__
|
||||
char **inherited = *_NSGetEnviron();
|
||||
#else
|
||||
char **inherited = environ;
|
||||
#endif
|
||||
size_t count = 0;
|
||||
int has_limit = 0;
|
||||
for (; inherited[count] != NULL; count++)
|
||||
if (strncmp(inherited[count], "TSC_TS_LISTEN_MEMORY=", 21) == 0) has_limit = 1;
|
||||
char **environment = malloc((count + 2) * sizeof(char *));
|
||||
if (environment == NULL) { error_text("out of memory"); return -1; }
|
||||
memcpy(environment, inherited, count * sizeof(char *));
|
||||
environment[count] = has_limit ? NULL : limit;
|
||||
environment[count + 1] = NULL;
|
||||
long descriptors = sysconf(_SC_OPEN_MAX);
|
||||
if (descriptors < 0 || descriptors > 65536) descriptors = 65536;
|
||||
sigset_t signals;
|
||||
sigemptyset(&signals);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
// Only async-signal-safe calls until exec.
|
||||
int null = open("/dev/null", O_RDWR);
|
||||
if (setpgid(0, 0) != 0 || null < 0 || dup2(null, STDIN_FILENO) < 0 || dup2(null, STDOUT_FILENO) < 0 ||
|
||||
dup2(null, STDERR_FILENO) < 0)
|
||||
_exit(127);
|
||||
#if defined(__linux__) && defined(SYS_close_range)
|
||||
if (syscall(SYS_close_range, 3U, ~0U, 0U) != 0)
|
||||
#endif
|
||||
for (long fd = 3; fd < descriptors; fd++) close((int)fd);
|
||||
sigprocmask(SIG_SETMASK, &signals, NULL);
|
||||
execve(args[0], args, environment);
|
||||
_exit(127);
|
||||
}
|
||||
int error = errno;
|
||||
free(environment);
|
||||
if (pid < 0) { system_error(error); return -1; }
|
||||
return pid;
|
||||
}
|
||||
|
||||
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *server = copy_argument(name, name_size, "");
|
||||
if (server == NULL) return 0;
|
||||
char directory[256], socket_path[sizeof(((struct sockaddr_un *)0)->sun_path)], lock_path[512];
|
||||
int named = strchr(server, '/') == NULL;
|
||||
if (!named) error_text("checker server names cannot contain '/'");
|
||||
if (named) named = server_directory(directory, sizeof(directory));
|
||||
if (named) {
|
||||
int count = snprintf(socket_path, sizeof(socket_path), "%s/%s.sock", directory, server);
|
||||
named = count > 0 && (size_t)count < sizeof(socket_path);
|
||||
if (!named) error_text("checker server socket path is too long");
|
||||
else (void)snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path);
|
||||
}
|
||||
free(server);
|
||||
if (!named) return 0;
|
||||
// One build at a time uses the server; a build that finds it in use
|
||||
// starts its own server instead of waiting.
|
||||
int lock = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||
if (lock < 0) { system_error(errno); return 0; }
|
||||
if (flock(lock, LOCK_EX | LOCK_NB) != 0) {
|
||||
if (errno == EWOULDBLOCK) error_text("the checker server is in use");
|
||||
else system_error(errno);
|
||||
close(lock);
|
||||
return 0;
|
||||
}
|
||||
int fd = connect_server(socket_path);
|
||||
if (fd < 0) {
|
||||
char *exe = copy_argument(executable, executable_size, "");
|
||||
char *dir = exe == NULL ? NULL : copy_argument(cwd, cwd_size, "");
|
||||
char *callback_flag = dir == NULL ? NULL : copy_argument(callbacks, callbacks_size, "--callbacks=");
|
||||
char idle[16], limit[48];
|
||||
(void)snprintf(idle, sizeof(idle), "%lu", (unsigned long)idle_exit);
|
||||
(void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory);
|
||||
pid_t pid = -1;
|
||||
if (callback_flag != NULL) {
|
||||
char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle};
|
||||
size_t argc = 9;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
pid = start_server(args, limit);
|
||||
}
|
||||
free(exe); free(dir); free(callback_flag);
|
||||
// The server listens as it starts. One that exits first, such as a
|
||||
// tsc-ts without --listen, or one that is not listening within 2 s,
|
||||
// leaves this build to start its own server.
|
||||
for (int attempt = 0; pid > 0; attempt++) {
|
||||
fd = connect_server(socket_path);
|
||||
if (fd >= 0) break;
|
||||
int status;
|
||||
pid_t result;
|
||||
do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR);
|
||||
if (result != 0) { error_text("the checker server exited as it started"); break; }
|
||||
if (attempt == 1000) { error_text("the checker server did not start listening"); break; }
|
||||
struct timespec delay = {0, 2000000};
|
||||
while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {}
|
||||
}
|
||||
}
|
||||
if (fd < 0) { close(lock); return 0; }
|
||||
Ts7Process *p = calloc(1, sizeof(*p));
|
||||
if (p == NULL) { error_text("out of memory"); close(fd); close(lock); return 0; }
|
||||
if (!exit_registered) {
|
||||
if (atexit(scriptc_ts7_close_all) != 0) {
|
||||
error_text("could not register server cleanup"); free(p); close(fd); close(lock); return 0;
|
||||
}
|
||||
exit_registered = 1;
|
||||
}
|
||||
p->socket = fd;
|
||||
p->lock = lock;
|
||||
p->id = next_id++;
|
||||
p->next = processes;
|
||||
processes = p;
|
||||
return p->id;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t scriptc_ts7_pid(uint32_t handle) {
|
||||
Ts7Process *p = find_process(handle);
|
||||
return p != NULL ? p->pid : 0;
|
||||
@@ -405,3 +605,47 @@ void scriptc_ts7_close(uint32_t handle) {
|
||||
void scriptc_ts7_close_all(void) {
|
||||
while (processes != NULL) scriptc_ts7_close(processes->id);
|
||||
}
|
||||
|
||||
static int json_space(uint8_t byte) {
|
||||
return byte == ' ' || byte == '\n' || byte == '\r' || byte == '\t';
|
||||
}
|
||||
|
||||
int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start) {
|
||||
if (size > INT32_MAX) return -1;
|
||||
size_t depth = 0;
|
||||
size_t index = start;
|
||||
while (index < size) {
|
||||
uint8_t byte = bytes[index];
|
||||
if (byte == '"') {
|
||||
index++;
|
||||
// The quote found stays the candidate end until an escape skips it, so
|
||||
// every byte of the string is scanned once by each search.
|
||||
const uint8_t *quote = index < size ? memchr(bytes + index, '"', size - index) : NULL;
|
||||
for (;;) {
|
||||
if (quote == NULL) return -1;
|
||||
const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index)));
|
||||
if (escape == NULL) {
|
||||
index = (size_t)(quote - bytes);
|
||||
break;
|
||||
}
|
||||
index = (size_t)(escape - bytes) + 2;
|
||||
if (bytes + index > quote) quote = index < size ? memchr(bytes + index, '"', size - index) : NULL;
|
||||
}
|
||||
index++;
|
||||
if (depth == 0) return (int32_t)index;
|
||||
continue;
|
||||
}
|
||||
if (byte == '{' || byte == '[') {
|
||||
depth++;
|
||||
} else if (byte == '}' || byte == ']') {
|
||||
if (depth == 0) return (int32_t)index;
|
||||
depth--;
|
||||
if (depth == 0) return (int32_t)(index + 1);
|
||||
} else if (depth == 0 && (byte == ',' || json_space(byte))) {
|
||||
return (int32_t)index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (depth != 0) return -1;
|
||||
return (int32_t)index;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeClose", "symbol": "scriptc_ts7_close", "params": ["u32"], "returns": "void" },
|
||||
{ "name": "ts7NativeCloseAll", "symbol": "scriptc_ts7_close_all", "params": [], "returns": "void" },
|
||||
{ "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" }
|
||||
{ "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" },
|
||||
{ "name": "ts7NativeJsonValueEnd", "symbol": "scriptc_ts7_json_value_end", "params": ["bytes", "u32"], "returns": "i32" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,21 @@
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing);
|
||||
uint8_t timing, uint8_t libraries_from_disk);
|
||||
/* Connects to the shared checker server `name` (`--api --listen`) in this
|
||||
* user's private server directory, starting it in its own process group
|
||||
* with `--idle-exit idle_exit` and a limit of `memory` megabytes
|
||||
* (TSC_TS_LISTEN_MEMORY, unless the environment sets it) when none answers. Returns 0 with the error
|
||||
* text when another build is connected to it, it cannot be started or
|
||||
* reached, or the platform has no Unix sockets; the caller then starts its
|
||||
* own server with scriptc_ts7_open. Closing the handle ends the connection
|
||||
* and leaves the server running. */
|
||||
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory);
|
||||
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
uint32_t offset, uint32_t length);
|
||||
int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size,
|
||||
@@ -21,5 +35,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle);
|
||||
void scriptc_ts7_close(uint32_t handle);
|
||||
void scriptc_ts7_close_all(void);
|
||||
int32_t scriptc_ts7_error(uint8_t *bytes, size_t size);
|
||||
/* The index after the JSON value that starts at `start` in a checker reply,
|
||||
* as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is
|
||||
* malformed there. */
|
||||
int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -19,12 +21,14 @@
|
||||
static const char *executable;
|
||||
static const char *callbacks = "readFile,fileExists";
|
||||
|
||||
static uint32_t start(const char *mode, int timing) {
|
||||
static uint32_t start_with(const char *mode, int timing, int libraries) {
|
||||
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode),
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing);
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
|
||||
}
|
||||
|
||||
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
|
||||
|
||||
static void fail_message(void) {
|
||||
uint8_t message[1024];
|
||||
int32_t size = scriptc_ts7_error(message, sizeof(message));
|
||||
@@ -81,11 +85,13 @@ static void argument_cases(void) {
|
||||
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (int timing = 0; timing <= 1; timing++) {
|
||||
for (int flags = 0; flags < 4; flags++) {
|
||||
int timing = flags & 1, libraries = flags >> 1;
|
||||
char mode[256], expected[512];
|
||||
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain");
|
||||
uint32_t id = start(mode, timing);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
|
||||
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
|
||||
uint32_t id = start_with(mode, timing, libraries);
|
||||
assert(id != 0);
|
||||
uint8_t actual[512];
|
||||
read_exact(id, actual, strlen(expected));
|
||||
@@ -145,10 +151,10 @@ static void failure_cases(void) {
|
||||
executable = "/scriptc/does-not-exist/tsgo";
|
||||
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
|
||||
executable = saved;
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
uint32_t id = start("exit", 0);
|
||||
assert(id != 0);
|
||||
@@ -171,6 +177,89 @@ static void failure_cases(void) {
|
||||
scriptc_ts7_close(UINT32_MAX);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
|
||||
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
|
||||
0, 0, 1, memory);
|
||||
}
|
||||
|
||||
static void expect_line(uint32_t id, const char *expected) {
|
||||
char line[512];
|
||||
size_t at = 0;
|
||||
do {
|
||||
assert(at + 1 < sizeof(line));
|
||||
read_exact(id, (uint8_t *)line + at, 1);
|
||||
} while (line[at++] != '\n');
|
||||
line[at] = '\0';
|
||||
assert(strcmp(line, expected) == 0);
|
||||
}
|
||||
|
||||
static void attach_cases(void) {
|
||||
char runtime[] = "/tmp/scriptc-ts7-attach-XXXXXX";
|
||||
assert(mkdtemp(runtime) != NULL);
|
||||
assert(setenv("XDG_RUNTIME_DIR", runtime, 1) == 0);
|
||||
assert(unsetenv("TSC_TS_LISTEN_MEMORY") == 0);
|
||||
char directory[256], socket_path[512], expected[600];
|
||||
uint8_t bytes[4];
|
||||
snprintf(directory, sizeof(directory), "%s/scriptc", runtime);
|
||||
snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory);
|
||||
// The first build starts the server with its idle time and memory limit.
|
||||
// The server holds none of the build's descriptors, such as an output
|
||||
// pipe its caller reads to the end.
|
||||
int output[2];
|
||||
assert(pipe(output) == 0);
|
||||
uint32_t first = attach("server", "listen", 123);
|
||||
assert(first != 0 && scriptc_ts7_pid(first) == 0);
|
||||
close(output[1]);
|
||||
struct pollfd ended = {output[0], POLLIN, 0};
|
||||
assert(poll(&ended, 1, 5000) == 1);
|
||||
assert(read(output[0], bytes, 1) == 0);
|
||||
close(output[0]);
|
||||
expect_line(first, "client 1\n");
|
||||
snprintf(expected, sizeof(expected), "%s 1\n", socket_path);
|
||||
expect_line(first, expected);
|
||||
expect_line(first, "123\n");
|
||||
write_exact(first, (const uint8_t *)"ping", 4, 0, 4);
|
||||
read_exact(first, bytes, 4);
|
||||
assert(memcmp(bytes, "ping", 4) == 0);
|
||||
// A build that finds the server in use gets no handle.
|
||||
for (int i = 0; i < 3; i++) { assert(attach("server", "listen", 123) == 0); fail_message(); }
|
||||
scriptc_ts7_close(first);
|
||||
scriptc_ts7_close(first);
|
||||
// A later build connects to the same server.
|
||||
uint32_t second = attach("server", "listen", 123);
|
||||
assert(second != 0);
|
||||
expect_line(second, "client 2\n");
|
||||
scriptc_ts7_close(second);
|
||||
// An executable without --listen exits as it starts.
|
||||
assert(attach("old", "listen-exit", 0) == 0);
|
||||
fail_message();
|
||||
// A server directory others can access is not used.
|
||||
assert(chmod(directory, 0755) == 0);
|
||||
assert(attach("server", "listen", 123) == 0);
|
||||
fail_message();
|
||||
assert(chmod(directory, 0700) == 0);
|
||||
// A name that would leave the directory, or make the path too long.
|
||||
assert(attach("../server", "listen", 123) == 0);
|
||||
fail_message();
|
||||
char long_name[200];
|
||||
memset(long_name, 'x', sizeof(long_name) - 1);
|
||||
long_name[sizeof(long_name) - 1] = '\0';
|
||||
assert(attach(long_name, "listen", 123) == 0);
|
||||
fail_message();
|
||||
// The server exits after its idle second and removes its socket.
|
||||
unlink(socket_path);
|
||||
snprintf(expected, sizeof(expected), "%s.lock", socket_path);
|
||||
unlink(expected);
|
||||
snprintf(expected, sizeof(expected), "%s/old.sock.lock", directory);
|
||||
unlink(expected);
|
||||
rmdir(directory);
|
||||
rmdir(runtime);
|
||||
assert(unsetenv("XDG_RUNTIME_DIR") == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int process_main(int argc, char **argv) {
|
||||
assert(argc >= 2);
|
||||
executable = argv[1];
|
||||
@@ -196,6 +285,9 @@ static int process_main(int argc, char **argv) {
|
||||
argument_cases();
|
||||
exchange_cases();
|
||||
failure_cases();
|
||||
#ifndef _WIN32
|
||||
attach_cases();
|
||||
#endif
|
||||
assert(resource_count() == before);
|
||||
#ifndef _WIN32
|
||||
if (saved_input >= 0) { assert(dup2(saved_input, STDIN_FILENO) >= 0); close(saved_input); }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -19,7 +19,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
inspect: features.inspect,
|
||||
dynInvoke: features.dynInvoke,
|
||||
@@ -61,6 +63,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
dh: features.dh,
|
||||
pki: features.pki,
|
||||
ed25519: features.ed25519,
|
||||
x509: features.x509,
|
||||
webcrypto: features.webcrypto,
|
||||
perfLoop: features.perfLoop,
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function compilePackedLibrary(
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: options.x509 ?? false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
tlsCa: false,
|
||||
@@ -152,7 +153,7 @@ export async function compilePackedLibrary(
|
||||
...(await Promise.all(
|
||||
shards
|
||||
.slice(start, start + 4)
|
||||
.map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)),
|
||||
.map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
installDarwinDebugSymbols,
|
||||
@@ -240,8 +241,11 @@ export async function linkNativeExecutable(
|
||||
try {
|
||||
const staged = await stageRuntimePackArtifacts(plan.runtimePack);
|
||||
stagedRoot = staged.root;
|
||||
// Linux links with lld when the default driver finds it.
|
||||
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
|
||||
const args = [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...plan.systemLibraries.map((name) => `-l${name}`),
|
||||
@@ -276,7 +280,15 @@ export async function linkNativeExecutable(
|
||||
...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])),
|
||||
])
|
||||
.catch(() => null);
|
||||
await execFileAsync(linker, args);
|
||||
await execFileAsync(linker, args).catch(async (error: unknown) => {
|
||||
// A failed link with lld runs again with the driver's default linker,
|
||||
// whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
await execFileAsync(
|
||||
linker,
|
||||
args.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
});
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
if (plan.darwinDebugSymbols) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function tool(path: string): string {
|
||||
writeFileSync(path, "#!/bin/sh\n");
|
||||
chmodSync(path, 0o755);
|
||||
return path;
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-lld-"));
|
||||
directories.push(root);
|
||||
const llvm = join(root, "llvm", "bin");
|
||||
const usr = join(root, "usr", "bin");
|
||||
const other = join(root, "other");
|
||||
for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true });
|
||||
return { root, llvm, usr, other };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a driver finds ld.lld beside itself, beside its real executable, or on PATH",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
// Without ld.lld anywhere.
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(false);
|
||||
// Beside the driver.
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(true);
|
||||
// A PATH entry that links to the driver: clang looks beside its real executable.
|
||||
symlinkSync(clang, join(f.usr, "clang"));
|
||||
expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true);
|
||||
// A driver elsewhere finds ld.lld on PATH, and not otherwise.
|
||||
const gcc = tool(join(f.other, "gcc"));
|
||||
expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false);
|
||||
expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true);
|
||||
expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"Linux links with the default driver use lld when the driver finds it",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
const env = { PATH: f.other };
|
||||
expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]);
|
||||
expect(lldLinkerArgs("darwin", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("windows", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The executable `command` names: a path, or the first match on PATH. */
|
||||
function commandFile(command: string, env: NodeJS.ProcessEnv): string | null {
|
||||
if (command.includes("/")) return isFile(command) ? resolve(command) : null;
|
||||
for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
|
||||
const candidate = join(directory || ".", command);
|
||||
if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in
|
||||
* the directory of the driver's executable (where clang looks first) or on
|
||||
* PATH. Linking still falls back to the driver's default linker when a link
|
||||
* with lld fails.
|
||||
*/
|
||||
export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const file = commandFile(driver, env);
|
||||
if (file === null) return false;
|
||||
try {
|
||||
for (const directory of new Set([dirname(file), dirname(realpathSync(file))]))
|
||||
if (isFile(join(directory, "ld.lld"))) return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return commandFile("ld.lld", env) !== null;
|
||||
}
|
||||
|
||||
/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */
|
||||
export function lldLinkerArgs(
|
||||
platform: string,
|
||||
driver: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env)
|
||||
? ["-fuse-ld=lld"]
|
||||
: [];
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -120,3 +120,36 @@ export function findAppendedArrayLiterals(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `const xs = []` directly followed by a capacity hint for `xs` (an
|
||||
* `arraySetLength` with `reserve`) whose length reads only literals and
|
||||
* other locals: the literal can be constructed with that capacity, and the
|
||||
* hint then has nothing left to do. */
|
||||
export function reservedArrayLength(
|
||||
stmts: readonly IrStmt[],
|
||||
i: number,
|
||||
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
|
||||
const decl = stmts[i];
|
||||
const next = stmts[i + 1];
|
||||
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
|
||||
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
|
||||
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
|
||||
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
|
||||
}
|
||||
|
||||
function plainLength(e: IrExpr, array: string): boolean {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return e.localId !== array;
|
||||
case "bin":
|
||||
return plainLength(e.left, array) && plainLength(e.right, array);
|
||||
case "ternary":
|
||||
return (
|
||||
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* code after it); here the lines are DROPPED instead, and emitStmts skips
|
||||
* whole statements once the current block is terminated, so no dropped
|
||||
* definition can leak into a later block. */
|
||||
import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export class BlockBuilder {
|
||||
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
|
||||
@@ -86,7 +87,18 @@ export class BlockBuilder {
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) {
|
||||
if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue);
|
||||
if (
|
||||
this.returnEpilogue &&
|
||||
(s.startsWith("ret ") ||
|
||||
s.startsWith("call void @scr_unwind_raise()") ||
|
||||
s === UNWIND_RESUME)
|
||||
)
|
||||
this.line(this.returnEpilogue);
|
||||
if (s === UNWIND_RESUME) {
|
||||
const exception = this.tmp();
|
||||
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
|
||||
s = `resume { ptr, i32 } ${exception}`;
|
||||
}
|
||||
this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
|
||||
}
|
||||
|
||||
/** Element storage's tag, and the alias scope of the access being emitted
|
||||
* in a loop version's fast copy (bytesAccessScope). */
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:elements");
|
||||
return host.memoryAliasAttachment("bytes:elements") + host.bytesAccessScope;
|
||||
}
|
||||
|
||||
/** A module global's slot is its own category for whole-slot loads of a
|
||||
|
||||
@@ -14,11 +14,25 @@ interface Bound extends Extent {
|
||||
minimum: number;
|
||||
}
|
||||
type Node = IrExpr | IrStmt;
|
||||
const FLIPPED: Readonly<Record<string, string>> = {
|
||||
"<": ">",
|
||||
"<=": ">=",
|
||||
">": "<",
|
||||
">=": "<=",
|
||||
"===": "===",
|
||||
"==": "==",
|
||||
"!==": "!==",
|
||||
"!=": "!=",
|
||||
};
|
||||
|
||||
/** Relational proofs for fixed typed-array lengths. Cached lengths retain
|
||||
* their receiver identity only when that binding is never reassigned.
|
||||
* Loops may use offsets in either direction, but both index integrality
|
||||
* and receiver stability are established separately from the relation. */
|
||||
* and receiver stability are established separately from the relation.
|
||||
* For loops hold one bound over the body. While loops start from the
|
||||
* condition's bound and follow the body in order: increments raise it and
|
||||
* comparisons with the length in if conditions narrow it. Indexes are
|
||||
* `counter + c` or `L + d - counter`, where L is the receiver's length. */
|
||||
export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): ReadonlySet<Node> {
|
||||
const proved = new Map<Node, boolean>();
|
||||
if (fn.async || fn.generator || !fn.locals.some((local) => local.type.kind === "bytes"))
|
||||
@@ -96,6 +110,20 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
|
||||
return Number.isSafeInteger(result) ? result : null;
|
||||
}
|
||||
/** For an index of the form `L + d - counter` (`len - 1 - i`, `right - i`
|
||||
* with `right = len - 1`), where L is the receiver's length: d. */
|
||||
function reversed(e: IrExpr, bound: Bound): number | null {
|
||||
if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-")) return null;
|
||||
if (e.op === "-" && e.right.kind === "varRef" && e.right.localId === bound.counter) {
|
||||
const length = extent(e.left, false);
|
||||
return length && length.receiver === bound.receiver ? length.offset : null;
|
||||
}
|
||||
if (e.right.kind !== "numLit" || !Number.isSafeInteger(e.right.value)) return null;
|
||||
const left = reversed(e.left, bound);
|
||||
if (left === null) return null;
|
||||
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
|
||||
return Number.isSafeInteger(result) ? result : null;
|
||||
}
|
||||
function affineRange(e: IrExpr, bound: Bound): IntegerRange | null {
|
||||
if (e.kind === "varRef" && e.localId === bound.counter) {
|
||||
const max = Number.MAX_SAFE_INTEGER + bound.offset - 1;
|
||||
@@ -159,7 +187,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
if (s.cond.left.kind !== "varRef" || s.cond.left.localId !== counter) {
|
||||
if (right.kind !== "varRef" || right.localId !== counter) return null;
|
||||
right = s.cond.left;
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
op = FLIPPED[op] ?? "";
|
||||
}
|
||||
const update = s.update;
|
||||
let step = 0;
|
||||
@@ -212,6 +240,98 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
}
|
||||
return result && stable(s.body, result.receiver, counter) ? result : null;
|
||||
}
|
||||
/** A comparison of the counter with the receiver's length: the operator
|
||||
* with the counter on the left, and the length's offset. */
|
||||
function lengthComparison(e: IrExpr, bound: Bound): { op: string; offset: number } | null {
|
||||
if (e.kind !== "bin") return null;
|
||||
let other = e.right,
|
||||
op: string = e.op;
|
||||
if (e.left.kind !== "varRef" || e.left.localId !== bound.counter) {
|
||||
if (other.kind !== "varRef" || other.localId !== bound.counter) return null;
|
||||
other = e.left;
|
||||
op = FLIPPED[op] ?? op;
|
||||
}
|
||||
const length = extent(other, false);
|
||||
return length && length.receiver === bound.receiver ? { op, offset: length.offset } : null;
|
||||
}
|
||||
/** The step of `counter = counter + c` with a positive literal c, or null. */
|
||||
function increment(s: IrStmt, counter: string): number | null {
|
||||
return s.kind === "assign" &&
|
||||
s.localId === counter &&
|
||||
s.value.kind === "bin" &&
|
||||
s.value.op === "+" &&
|
||||
s.value.left.kind === "varRef" &&
|
||||
s.value.left.localId === counter &&
|
||||
s.value.right.kind === "numLit" &&
|
||||
Number.isSafeInteger(s.value.right.value) &&
|
||||
s.value.right.value > 0
|
||||
? s.value.right.value
|
||||
: null;
|
||||
}
|
||||
function writes(s: IrStmt, id: string): boolean {
|
||||
return !everyStmtList([s], {
|
||||
expr: (e) => (e.kind !== "assignExpr" && e.kind !== "incDec") || e.localId !== id,
|
||||
stmt: (child) =>
|
||||
!(
|
||||
((child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") &&
|
||||
child.localId === id) ||
|
||||
(child.kind === "tryCatch" && child.catchLocalId === id)
|
||||
),
|
||||
});
|
||||
}
|
||||
/** A while loop whose condition bounds an integer counter by a typed
|
||||
* array's length. The body may write the counter only by statements
|
||||
* that add a positive literal, and may not write the receiver. Calls
|
||||
* cannot write either binding (both are uncaptured locals), and a typed
|
||||
* array's length is fixed at construction, so the body may hold calls. */
|
||||
function whileBound(s: IrStmt & { kind: "while" }): Bound | null {
|
||||
const cond = s.cond;
|
||||
if (cond.kind !== "bin") return null;
|
||||
const counterRef = [cond.left, cond.right].find(
|
||||
(side, i) =>
|
||||
side.kind === "varRef" &&
|
||||
eligible(side.localId) &&
|
||||
extent(i === 0 ? cond.right : cond.left, false) !== null,
|
||||
);
|
||||
const minimum = counterRef ? ranges.get(counterRef)?.min : undefined;
|
||||
if (counterRef?.kind !== "varRef" || minimum === undefined) return null;
|
||||
const counter = counterRef.localId;
|
||||
const length = extent(counterRef === cond.left ? cond.right : cond.left, false)!;
|
||||
const receiver = length.receiver;
|
||||
const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum });
|
||||
if (comparison?.op !== "<" && comparison?.op !== "<=") return null;
|
||||
// An increment is a statement of the body: one inside an expression
|
||||
// (a comma expression's statements) would run where flow does not
|
||||
// follow it.
|
||||
const writesOk = everyStmtList(s.body, {
|
||||
expr: (e) =>
|
||||
((e.kind !== "assignExpr" && e.kind !== "incDec") ||
|
||||
(e.localId !== counter && e.localId !== receiver)) &&
|
||||
everyExprChild(
|
||||
e,
|
||||
() => true,
|
||||
(child) => !writes(child, counter),
|
||||
),
|
||||
stmt: (child) => {
|
||||
if (child.kind === "assign" && child.localId === counter)
|
||||
return increment(child, counter) !== null;
|
||||
if (child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl")
|
||||
return child.localId !== counter && child.localId !== receiver;
|
||||
return (
|
||||
child.kind !== "tryCatch" ||
|
||||
(child.catchLocalId !== counter && child.catchLocalId !== receiver)
|
||||
);
|
||||
},
|
||||
});
|
||||
return writesOk
|
||||
? {
|
||||
receiver,
|
||||
counter,
|
||||
offset: comparison.offset + (comparison.op === "<=" ? 1 : 0),
|
||||
minimum,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void {
|
||||
const integer = ranges.get(index);
|
||||
let safe = false;
|
||||
@@ -228,6 +348,13 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
safe = true;
|
||||
break;
|
||||
}
|
||||
// counter < L + bound.offset and counter >= bound.minimum put
|
||||
// L + d - counter in [0, L - width].
|
||||
const d = integer ? reversed(index, bound) : null;
|
||||
if (d !== null && bound.offset <= d + 1 && bound.minimum >= d + width) {
|
||||
safe = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
proved.set(node, (proved.get(node) ?? true) && safe);
|
||||
@@ -244,8 +371,84 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
(child) => stmt(child, active),
|
||||
);
|
||||
}
|
||||
/** The bounds where `cond` is true and where it is false, from the
|
||||
* bound before it. */
|
||||
function narrow(cond: IrExpr, bound: Bound): [Bound, Bound] {
|
||||
const comparison = lengthComparison(cond, bound);
|
||||
if (!comparison) return [bound, bound];
|
||||
const j = comparison.offset,
|
||||
offset = bound.offset;
|
||||
// counter < L + j, counter <= L + j, and counter !== L + j.
|
||||
const below = { ...bound, offset: Math.min(offset, j) },
|
||||
atMost = { ...bound, offset: Math.min(offset, j + 1) },
|
||||
unequal = offset === j + 1 ? { ...bound, offset: j } : bound;
|
||||
switch (comparison.op) {
|
||||
case "<":
|
||||
return [below, bound];
|
||||
case "<=":
|
||||
return [atMost, bound];
|
||||
case ">":
|
||||
return [bound, atMost];
|
||||
case ">=":
|
||||
return [bound, below];
|
||||
case "===":
|
||||
case "==":
|
||||
return [atMost, unequal];
|
||||
case "!==":
|
||||
case "!=":
|
||||
return [unequal, atMost];
|
||||
default:
|
||||
return [bound, bound];
|
||||
}
|
||||
}
|
||||
/** Visits a while body in order, from the bound that holds on entry.
|
||||
* Returns the bound at the end of the list. Its offset is -Infinity (and
|
||||
* its minimum Infinity) when the end is unreachable, and Infinity when
|
||||
* nothing bounds the counter there. Every write of the counter adds a
|
||||
* positive literal (whileBound), so the minimum survives any write. */
|
||||
function flow(list: IrStmt[], active: Bound[], bound: Bound): Bound {
|
||||
for (const s of list) {
|
||||
const current = Number.isFinite(bound.offset) ? [...active, bound] : active;
|
||||
const step = increment(s, bound.counter);
|
||||
if (step !== null) {
|
||||
bound = { ...bound, offset: bound.offset + step, minimum: bound.minimum + step };
|
||||
} else if (s.kind === "if") {
|
||||
expr(s.cond, current);
|
||||
const [then, else_] = narrow(s.cond, bound);
|
||||
const left = flow(s.then, active, then),
|
||||
right = s.else_ ? flow(s.else_, active, else_) : else_;
|
||||
bound = {
|
||||
...bound,
|
||||
offset: Math.max(left.offset, right.offset),
|
||||
minimum: Math.min(left.minimum, right.minimum),
|
||||
};
|
||||
} else if (s.kind === "block" && !s.labels?.length) {
|
||||
bound = flow(s.body, active, bound);
|
||||
} else if (writes(s, bound.counter)) {
|
||||
stmt(s, active);
|
||||
bound = { ...bound, offset: Infinity };
|
||||
} else {
|
||||
stmt(s, current);
|
||||
if (
|
||||
s.kind === "break" ||
|
||||
s.kind === "continue" ||
|
||||
s.kind === "return" ||
|
||||
s.kind === "throw"
|
||||
)
|
||||
bound = { ...bound, offset: -Infinity, minimum: Infinity };
|
||||
}
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
function stmt(s: IrStmt, active: Bound[]): boolean {
|
||||
if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1);
|
||||
if (s.kind === "while") {
|
||||
expr(s.cond, active);
|
||||
const bound = whileBound(s);
|
||||
if (!bound) return s.body.every((child) => stmt(child, active));
|
||||
flow(s.body, active, bound);
|
||||
return true;
|
||||
}
|
||||
if (s.kind === "for") {
|
||||
if (s.init) stmt(s.init, active);
|
||||
if (s.cond) expr(s.cond, active);
|
||||
|
||||
@@ -317,13 +317,19 @@ test("worker programs test each receiver's shared storage once in the guard", ()
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.fast\.shared\d+:$/m.exec(body)![0];
|
||||
expect(fast).not.toContain("bytes.shared");
|
||||
expect(fast).not.toMatch(/(load|store) atomic double/);
|
||||
// The checked copy keeps the shared-memory path for each access.
|
||||
// The checked copy keeps the shared-memory path for each access, and so
|
||||
// does the copy for shared views whose bounds hold.
|
||||
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");
|
||||
});
|
||||
|
||||
test("index temps of a version's counter are integers in the fast copy", () => {
|
||||
@@ -580,7 +586,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () =>
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m));
|
||||
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
|
||||
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
|
||||
// The byte copy, up to the units copy that follows it.
|
||||
const units = body.search(/^bytes\.version\.fast\.units\d+:/m);
|
||||
const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units);
|
||||
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
||||
expect(fast).not.toContain("@sc_str_char_code_at_int");
|
||||
// The checked copy keeps the inline helper and its fallback.
|
||||
@@ -699,7 +707,8 @@ test("a two-counter loop versions the typed arrays it indexes at either counter"
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
expect(guard).toMatch(/icmp sge i64 %\S+, 0/);
|
||||
// `i` starts at the literal 0, so its test compares the integer itself.
|
||||
expect(guard).toMatch(/icmp sge i64 0, 0/);
|
||||
expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
// The fast copy keeps only the `src[i + 1]` check, and no second guard.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
BYTES_ELEMENT_SIZE,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
@@ -9,10 +16,18 @@ import {
|
||||
emitCountedLoopLimit,
|
||||
emitCountedLoopSafety,
|
||||
emitExactIntegerTest,
|
||||
emitStartIntegerTest,
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
STRING_SCAN_UNITS_DECL,
|
||||
stringScanType,
|
||||
} from "./string-reads.js";
|
||||
import { unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -71,8 +86,23 @@ export interface BytesLoopVersion {
|
||||
/** Root receiver bindings, read once before the loop; `string` marks a
|
||||
* string whose `charCodeAt` and `charAt` reads the version covers. */
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
/** Receivers the guard requires to have no shared storage for the fast
|
||||
* copy. */
|
||||
ownedReceivers: number[];
|
||||
/** Worker programs: when the bounds hold but a receiver in
|
||||
* `ownedReceivers` has shared storage, the loop runs a second fast copy
|
||||
* instead of the checked one. It skips the bounds checks of `bounds` and
|
||||
* keeps shared-safe accesses (emitBytesLoopVersionGuard's `sharedFast`). Set
|
||||
* when the version proves bounds as well as ownership. */
|
||||
sharedCopy: boolean;
|
||||
/** The string reads among `bounds`. */
|
||||
stringReads: ReadonlySet<Node>;
|
||||
/** The version's one string receiver, when the loop may also run over
|
||||
* its UTF-16 units if it is not ASCII (string-unit-versions.ts). */
|
||||
unitsReceiver: number | null;
|
||||
/** The units receiver's `length` reads in the loop. Both fast copies
|
||||
* read the length the guard took (its UTF-16 length) in their place. */
|
||||
unitLengths: ReadonlySet<IrExpr>;
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** While loops: accesses at a counter below a loop-invariant bound. */
|
||||
@@ -92,6 +122,12 @@ export interface BytesLoopVersion {
|
||||
/** Integer ranges of affine index operands that hold in the unchecked
|
||||
* loop (where the guard bounds the counter). */
|
||||
ranges: ReadonlyMap<IrExpr, IntegerRange>;
|
||||
/** Typed-array receivers whose element ranges the guard proves disjoint,
|
||||
* and the receiver of each of their element reads and writes. The fast
|
||||
* copy gives each receiver its own alias scope, so a store to one view
|
||||
* leaves the elements of the others in registers. Null when the loop
|
||||
* writes no element, or reads and writes a single view. */
|
||||
scopes: { receivers: number[]; nodes: ReadonlyMap<Node, number> } | null;
|
||||
}
|
||||
|
||||
export interface BytesLoopFacts {
|
||||
@@ -114,11 +150,16 @@ export interface BytesLoopFacts {
|
||||
/** String bindings held as a slice of another string (string-slices.ts):
|
||||
* they have no string header for the guard to read. */
|
||||
scalarStrings: ReadonlySet<string>;
|
||||
/** The functions a loop over a string's UTF-16 units may call; absent
|
||||
* or null where emitted code does not read the runtime's scan receiver. */
|
||||
unitSafeFunctions?: ReadonlySet<string> | null;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
const MAX_FIXED_END = 2 ** 31;
|
||||
const MAX_CHECKS = 8;
|
||||
/** Alias-scoped views per loop: the guard tests every pair's ranges. */
|
||||
const MAX_SCOPED_RECEIVERS = 6;
|
||||
const MAX_OFFSET = 2 ** 31;
|
||||
const SAFE = Number.MAX_SAFE_INTEGER;
|
||||
/** The range the guard requires of an invariant index term without a
|
||||
@@ -530,11 +571,24 @@ export function matchBytesLoopVersion(
|
||||
group.nodes.push(node);
|
||||
} else fixed.set(receiver, { check: { receiver, end }, nodes: [node] });
|
||||
};
|
||||
// Element reads and writes of each typed-array receiver, for alias scopes.
|
||||
const scoped = new Map<Node, number>();
|
||||
const writtenViews = new Set<number>();
|
||||
const scope = (node: Node, array: IrExpr, write: boolean): void => {
|
||||
const ref = root(array);
|
||||
if (!ref || ref.type.kind !== "bytes") return;
|
||||
const receiver = receiverOf(ref);
|
||||
scoped.set(node, receiver);
|
||||
if (write) writtenViews.add(receiver);
|
||||
};
|
||||
const visitExpr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "bytesIntrinsic") {
|
||||
const numeric = byteNumberAccess(e);
|
||||
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
|
||||
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
|
||||
else if (e.method === "get" && e.args[0]) {
|
||||
access(e, e.receiver, e.args[0], 1);
|
||||
scope(e, e.receiver, false);
|
||||
}
|
||||
} else if (
|
||||
e.kind === "strIntrinsic" &&
|
||||
(e.method === "charCodeAt" || e.method === "charAt") &&
|
||||
@@ -544,7 +598,10 @@ export function matchBytesLoopVersion(
|
||||
return everyExprChild(e, visitExpr, visitStmt);
|
||||
};
|
||||
const visitStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet") access(s, s.arr, s.index, 1);
|
||||
if (s.kind === "bytesSet") {
|
||||
access(s, s.arr, s.index, 1);
|
||||
scope(s, s.arr, true);
|
||||
}
|
||||
return everyStmtChild(s, visitExpr, visitStmt);
|
||||
};
|
||||
loop.body.every(visitStmt);
|
||||
@@ -667,9 +724,33 @@ export function matchBytesLoopVersion(
|
||||
: [],
|
||||
});
|
||||
}
|
||||
// Alias scopes: at least two views, one of them written, each read only
|
||||
// through plain loads (in a worker program, a view the guard proves
|
||||
// thread-owned).
|
||||
const scopedReceivers = [...new Set(scoped.values())].sort((a, b) => a - b);
|
||||
const ownedSet = new Set(ownedKept.map(([receiver]) => receiver));
|
||||
const scoping =
|
||||
scopedReceivers.length >= 2 &&
|
||||
scopedReceivers.length <= MAX_SCOPED_RECEIVERS &&
|
||||
scopedReceivers.some((receiver) => writtenViews.has(receiver)) &&
|
||||
(!facts.workers || scopedReceivers.every((receiver) => ownedSet.has(receiver)));
|
||||
if (scoping) for (const receiver of scopedReceivers) used.add(receiver);
|
||||
// Renumber the receivers the kept checks use.
|
||||
const order = [...used].sort((a, b) => a - b);
|
||||
const renumber = new Map(order.map((old, index) => [old, index]));
|
||||
const stringReads = new Set<Node>();
|
||||
for (const group of [...fixedKept, ...affineKept])
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const node of group.nodes) stringReads.add(node);
|
||||
for (const group of boundedKept)
|
||||
if (receivers[group.check.receiver]!.string)
|
||||
for (const { node } of group.nodes) stringReads.add(node);
|
||||
const strings = order.filter((index) => receivers[index]!.string);
|
||||
const unitLengths =
|
||||
facts.unitSafeFunctions != null && strings.length === 1
|
||||
? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions)
|
||||
: null;
|
||||
const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null;
|
||||
const needsBound =
|
||||
affineKept.length > 0 &&
|
||||
counted !== null &&
|
||||
@@ -679,6 +760,10 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
sharedCopy: ownedKept.length > 0 && bounds.size > 0,
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
@@ -704,6 +789,12 @@ export function matchBytesLoopVersion(
|
||||
: null,
|
||||
counterBound: needsBound ? bound : null,
|
||||
ranges,
|
||||
scopes: scoping
|
||||
? {
|
||||
receivers: scopedReceivers.map((receiver) => renumber.get(receiver)!),
|
||||
nodes: new Map([...scoped].map(([node, receiver]) => [node, renumber.get(receiver)!])),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1018,13 +1109,27 @@ function findWhileCounterAccesses(
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration and every view it proves thread-owned has no shared
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
* integer form is not established, or a module receiver not yet set).
|
||||
*
|
||||
* With `sharedFast` (a version with `sharedCopy`), the ownership tests are
|
||||
* separate: when everything else holds but a view has shared storage, the
|
||||
* guard branches to `sharedFast` instead of `slow`.
|
||||
*
|
||||
* With `unitsFast`, the version's units receiver may instead be a string
|
||||
* that is not ASCII: the guard has the runtime publish it as the scan
|
||||
* receiver (one read), tests the same accesses against its UTF-16 length,
|
||||
* and branches to `unitsFast`. It returns the units pointer that copy
|
||||
* reads, which stays valid while the loop runs (string-unit-versions.ts),
|
||||
* and for a version with a units receiver, the receiver's UTF-16 length
|
||||
* on both fast paths (an i64). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
fast: string,
|
||||
slow: string,
|
||||
): void {
|
||||
unitsFast?: string,
|
||||
sharedFast?: string,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
@@ -1043,30 +1148,75 @@ export function emitBytesLoopVersionGuard(
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
const len = version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver);
|
||||
const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null;
|
||||
const units: { read: { pointer: string; taken: string; published: string } | null } = {
|
||||
read: null,
|
||||
};
|
||||
const widen = (len: string): string => {
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
};
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
if (index === unitsIndex) {
|
||||
const read = emitUnitsLength(host, receiver, () => emitUnitsSpan(host, version, index));
|
||||
units.read = read;
|
||||
return read.length;
|
||||
}
|
||||
return widen(
|
||||
version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver),
|
||||
);
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
// The published count is the receiver's UTF-16 length only while the
|
||||
// runtime publishes the receiver itself.
|
||||
if (units.read) conditions.push(units.read.published);
|
||||
// A string's byte length is its UTF-16 length when it is ASCII.
|
||||
version.receivers.forEach(({ string }, index) => {
|
||||
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
if (string && index !== unitsIndex)
|
||||
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
// Without `sharedFast`, shared storage sends the loop to the checked copy
|
||||
// with the other failed tests; with it, only to the shared fast copy.
|
||||
const ownership: string[] = [];
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
|
||||
conditions.push(owned);
|
||||
(sharedFast !== undefined ? ownership : conditions).push(owned);
|
||||
}
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
|
||||
conditions.push(ok);
|
||||
}
|
||||
if (version.scopes) {
|
||||
// Every pair of scoped views covers disjoint bytes: one ends at or
|
||||
// before the other starts. An empty view covers none.
|
||||
const spans = version.scopes.receivers.map((index) => {
|
||||
const type = version.receivers[index]!.ref.type as IrType & { kind: "bytes" };
|
||||
const start = B.tmp(),
|
||||
size = B.tmp(),
|
||||
end = B.tmp();
|
||||
B.line(`${start} = ptrtoint ptr ${emitBytesData(host, receivers[index]!)} to i64`);
|
||||
B.line(`${size} = mul nuw i64 ${lengths[index]}, ${BYTES_ELEMENT_SIZE[type.elem]}`);
|
||||
B.line(`${end} = add nuw i64 ${start}, ${size}`);
|
||||
return { start, end };
|
||||
});
|
||||
spans.forEach((a, i) => {
|
||||
for (const b of spans.slice(i + 1)) {
|
||||
const before = B.tmp(),
|
||||
after = B.tmp(),
|
||||
apart = B.tmp();
|
||||
B.line(`${before} = icmp ule i64 ${a.end}, ${b.start}`);
|
||||
B.line(`${after} = icmp ule i64 ${b.end}, ${a.start}`);
|
||||
B.line(`${apart} = or i1 ${before}, ${after}`);
|
||||
conditions.push(apart);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const counter of version.counters) {
|
||||
const value = host.emitExpr(counter.ref);
|
||||
// A counter held as an integer (an int32 slot) is exact already and
|
||||
@@ -1158,7 +1308,7 @@ export function emitBytesLoopVersionGuard(
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
B.br(slow);
|
||||
B.startBlock(B.newLabel("bytes.version.dead"));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const wide = widenInteger(host, integer);
|
||||
const sum = B.tmp();
|
||||
@@ -1192,8 +1342,8 @@ export function emitBytesLoopVersionGuard(
|
||||
// access at either one is in bounds when `lo >= 0` and `hi < length`.
|
||||
const { loop } = version.paired;
|
||||
const [low, high] = [loop.low.start, loop.high.start].map((expr) => {
|
||||
const tested = emitExactIntegerTest(host, host.emitExpr(expr).name, -SAFE, SAFE);
|
||||
conditions.push(tested.ok);
|
||||
const tested = emitStartIntegerTest(host, expr, -SAFE, SAFE);
|
||||
if (tested.ok !== "true") conditions.push(tested.ok);
|
||||
return tested.integer;
|
||||
});
|
||||
const nonNegative = B.tmp();
|
||||
@@ -1215,8 +1365,134 @@ export function emitBytesLoopVersionGuard(
|
||||
valid = all;
|
||||
}
|
||||
}
|
||||
if (valid === "true") B.br(fast);
|
||||
else B.condBr(valid, fast, slow);
|
||||
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
|
||||
const taken = units.read;
|
||||
const target = taken ? B.newLabel("bytes.version.units") : fast;
|
||||
if (sharedFast !== undefined && taken)
|
||||
throw new InternalCompilerError("llvm emitter bug: a shared fast copy with a units copy");
|
||||
if (sharedFast !== undefined && ownership.length > 0) {
|
||||
// The bounds hold: unshared views take the owned copy.
|
||||
const split = B.newLabel("bytes.version.owned");
|
||||
let owned = ownership[0]!;
|
||||
for (const cond of ownership.slice(1)) {
|
||||
const all = B.tmp();
|
||||
B.line(`${all} = and i1 ${owned}, ${cond}`);
|
||||
owned = all;
|
||||
}
|
||||
if (valid === "true") B.br(split);
|
||||
else B.condBr(valid, split, slow);
|
||||
B.startBlock(split);
|
||||
B.condBr(owned, target, sharedFast);
|
||||
} else if (valid === "true") B.br(target);
|
||||
else B.condBr(valid, target, slow);
|
||||
if (!taken) return { units: null, length };
|
||||
B.startBlock(target);
|
||||
B.condBr(taken.taken, unitsFast!, fast);
|
||||
return { units: taken.pointer, length };
|
||||
}
|
||||
|
||||
/** The range of indexes a version's loop reads from its units receiver, as
|
||||
* two doubles: a counted loop's start and limit, or a while loop's one
|
||||
* counter and the receiver's bound. Both NaN when the version gives no such
|
||||
* range. The runtime converts the receiver at once only when the range is
|
||||
* long enough to pay for it (scr_str_scan_units_for). */
|
||||
function emitUnitsSpan(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
receiver: number,
|
||||
): { from: string; to: string } {
|
||||
// Locals and literals only: they read no string (a `length` of the
|
||||
// receiver would measure it). Anything else leaves the range unbounded.
|
||||
const number = (expr: IrExpr): string | null => {
|
||||
if (expr.kind !== "varRef" && expr.kind !== "numLit") return null;
|
||||
const value = host.emitExpr(expr);
|
||||
return value.type.kind === "f64" ? value.name : null;
|
||||
};
|
||||
const unbounded = { from: "0x7FF8000000000000", to: "0x7FF8000000000000" };
|
||||
const counted = version.counted;
|
||||
let from: IrExpr, to: IrExpr;
|
||||
if (counted) [from, to] = [counted.start, counted.limit];
|
||||
else {
|
||||
const bounded = version.bounded.filter((check) => check.receiver === receiver);
|
||||
if (version.counters.length !== 1 || bounded.length !== 1 || version.paired !== null)
|
||||
return unbounded;
|
||||
[from, to] = [version.counters[0]!.ref, bounded[0]!.bound];
|
||||
}
|
||||
const start = number(from),
|
||||
limit = number(to);
|
||||
return start !== null && limit !== null ? { from: start, to: limit } : unbounded;
|
||||
}
|
||||
|
||||
/** The length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, once it is the scan receiver, the
|
||||
* published unit count (0 when the runtime does not hold its units). When
|
||||
* `s` is not already the scan receiver, the runtime converts it to units
|
||||
* and publishes it if the loop's `span` makes that pay
|
||||
* (scr_str_scan_units_for). Also the units pointer, whether the loop is to
|
||||
* read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: string,
|
||||
span: () => { from: string; to: string },
|
||||
): { length: string; pointer: string; taken: string; published: string } {
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
const type = stringScanType(sz);
|
||||
const ascii = emitStringAsciiFlag(host, s);
|
||||
const bytes = emitStringByteLength(host, s);
|
||||
const from = B.currentLabel();
|
||||
const publish = B.newLabel("bytes.version.publish"),
|
||||
join = B.newLabel("bytes.version.length");
|
||||
B.condBr(ascii, join, publish);
|
||||
B.startBlock(publish);
|
||||
// When `s` is already the scan receiver (an earlier loop over it published
|
||||
// it), its units stay published until the runtime clears the scan: skip
|
||||
// the call. Loops re-entered over one string take that path; the weights
|
||||
// keep the call and its reach out of the loop's code.
|
||||
const held = B.tmp(),
|
||||
holds = B.tmp();
|
||||
B.line(`${held} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${holds} = icmp eq ptr ${held}, ${s}`);
|
||||
const call = B.newLabel("bytes.version.publish.units"),
|
||||
load = B.newLabel("bytes.version.publish.load");
|
||||
B.condBr(holds, load, call, `, !prof !{!"branch_weights", i32 1000, i32 1}`);
|
||||
B.startBlock(call);
|
||||
host.declare(STRING_SCAN_UNITS_DECL);
|
||||
const receiver = B.tmp(),
|
||||
same = B.tmp(),
|
||||
pointerSlot = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
const { from: first, to: bound } = span();
|
||||
B.line(`call void @scr_str_scan_units_for(ptr ${s}, double ${first}, double ${bound})`);
|
||||
B.br(load);
|
||||
B.startBlock(load);
|
||||
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${same} = icmp eq ptr ${receiver}, ${s}`);
|
||||
B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`);
|
||||
B.line(`${pointer} = load ptr, ptr ${pointerSlot}`);
|
||||
B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`);
|
||||
B.line(`${count} = load ${sz}, ptr ${countSlot}`);
|
||||
B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`);
|
||||
const end = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const length = B.tmp(),
|
||||
units = B.tmp(),
|
||||
taken = B.tmp();
|
||||
B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`);
|
||||
B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`);
|
||||
B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`);
|
||||
const exact = B.tmp();
|
||||
B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`);
|
||||
let wide = length;
|
||||
if (sz !== "i64") {
|
||||
wide = B.tmp();
|
||||
B.line(`${wide} = zext ${sz} ${length} to i64`);
|
||||
}
|
||||
return { length: wide, pointer: units, taken, published: exact };
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
|
||||
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
|
||||
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
|
||||
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
|
||||
});
|
||||
|
||||
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
|
||||
const wrap = (id: string): IrExpr => ({
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref(id, record),
|
||||
type: optional,
|
||||
loc,
|
||||
});
|
||||
const leaf = helper("leaf");
|
||||
const outer = helper("outer");
|
||||
outer.params[0]!.type = outer.locals[0]!.type = record;
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
|
||||
ret(call("leaf", [wrap("cell")])),
|
||||
];
|
||||
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
|
||||
const analyzeWith = (pointer: boolean) =>
|
||||
analyzeCallLifetimes(
|
||||
functions,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => pointer,
|
||||
);
|
||||
// A boxed union builds a new box around the payload: a whole-value use.
|
||||
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
|
||||
// A pointer union passes the payload pointer itself.
|
||||
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
|
||||
// A callee that keeps its parameter keeps the wrapped binding too.
|
||||
leaf.body.push(ret(ref("value")));
|
||||
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
|
||||
const wrapCell: IrExpr = {
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref("cell", record),
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
const outer = helper("outer", []);
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
ret(call("leaf", [wrapCell])),
|
||||
];
|
||||
const read = (value: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: value,
|
||||
shapeId: "cell",
|
||||
field: "x",
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
|
||||
const result = analyzeCallLifetimes(
|
||||
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
|
||||
(className) => className === "Node",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => true,
|
||||
);
|
||||
return result.locals.get("outer")?.has("cell") !== true;
|
||||
};
|
||||
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
|
||||
// Tag tests and field reads of the narrowed payload leave it in place.
|
||||
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
|
||||
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
|
||||
// So do field reads through a checked narrowing, which throws on the
|
||||
// other arm (`value.x` of a `T | undefined` parameter).
|
||||
const checked: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: tag("value"),
|
||||
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
|
||||
else_: narrow("value"),
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
|
||||
// Returning it keeps the payload.
|
||||
const returnsChecked = leafWith([ret(checked)]);
|
||||
returnsChecked.returnType = record;
|
||||
expect(keeps(returnsChecked)).toBe(true);
|
||||
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
|
||||
// although the union itself is only projected.
|
||||
const returned = leafWith([ret(narrow("value"))]);
|
||||
returned.returnType = record;
|
||||
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
|
||||
expect(keeps(returned)).toBe(true);
|
||||
const defaulted = leafWith([
|
||||
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
|
||||
]);
|
||||
defaulted.locals.push(local("other", record));
|
||||
expect(keeps(defaulted)).toBe(true);
|
||||
// So does a nullable-pointer field store of the union.
|
||||
const self: IrType = { kind: "object", className: "Node" };
|
||||
const stored = leafWith([
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: ref("this", self),
|
||||
className: "Node",
|
||||
field: "next",
|
||||
value: ref("value"),
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
stored.params = [{ localId: "value", name: "value", type: optional }];
|
||||
stored.locals.push(local("this", self));
|
||||
expect(keeps(stored)).toBe(true);
|
||||
// A narrowed payload passed on, or the union passed on, is in place when
|
||||
// the next callee keeps it in place.
|
||||
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
|
||||
const fn = helper("inner");
|
||||
fn.params[0]!.type = fn.locals[0]!.type = type;
|
||||
fn.body = body;
|
||||
return fn;
|
||||
};
|
||||
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
|
||||
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
|
||||
const returnsRecord = inner([ret(ref("value", record))]);
|
||||
returnsRecord.returnType = record;
|
||||
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
|
||||
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
|
||||
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
|
||||
const innerReturned = inner([ret(narrow("value"))], optional);
|
||||
innerReturned.returnType = record;
|
||||
expect(keeps(passUnion, innerReturned)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
|
||||
interface ForwardedUse {
|
||||
callee: string;
|
||||
index: number;
|
||||
/** The binding reached the call inside a nullable-pointer union
|
||||
* (PointerWrapTest): the callee's parameter holds the binding itself as
|
||||
* its payload, so the callee must keep the payload, not only the union,
|
||||
* in place. */
|
||||
wrapped: boolean;
|
||||
}
|
||||
|
||||
interface Uses {
|
||||
@@ -21,6 +26,13 @@ interface Uses {
|
||||
written: Set<string>;
|
||||
declarations: Map<string, number>;
|
||||
forwards: Map<string, ForwardedUse[]>;
|
||||
/** Union bindings whose payload a use may keep: a narrowed read that is
|
||||
* not projected or passed on, a narrowed nullish read, a store into a
|
||||
* nullable-pointer field. The union itself can still be only projected. */
|
||||
payloadKept: Set<string>;
|
||||
/** Narrowed payloads of union bindings passed directly as call arguments:
|
||||
* the payload stays in place if the callee's parameter is proved. */
|
||||
payloadForwards: Map<string, ForwardedUse[]>;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
|
||||
* reads the record and keeps no reference to it. */
|
||||
export type RecordDynCopyTest = (shapeId: string) => boolean;
|
||||
|
||||
/** Wraps into a nullable-pointer union whose value is the payload pointer
|
||||
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
|
||||
* callee's parameter unchanged. */
|
||||
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
|
||||
|
||||
function collectUses(
|
||||
fn: IrFunction,
|
||||
nullableField: NullableFieldTest,
|
||||
registerReturn: boolean,
|
||||
inlineField: InlineFieldTest,
|
||||
recordDynCopy: RecordDynCopyTest,
|
||||
pointerWrap: PointerWrapTest,
|
||||
): Uses {
|
||||
const uses: Uses = {
|
||||
invalid: new Set(),
|
||||
written: new Set(),
|
||||
declarations: new Map(),
|
||||
forwards: new Map(),
|
||||
payloadKept: new Set(),
|
||||
payloadForwards: new Map(),
|
||||
};
|
||||
const forward = (
|
||||
table: Map<string, ForwardedUse[]>,
|
||||
localId: string,
|
||||
use: ForwardedUse,
|
||||
): void => {
|
||||
let forwards = table.get(localId);
|
||||
if (!forwards) table.set(localId, (forwards = []));
|
||||
forwards.push(use);
|
||||
};
|
||||
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
|
||||
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
|
||||
* order), whose other branch is visited as any use. */
|
||||
function narrowedBinding(value: IrExpr): string | null {
|
||||
if (value.kind === "unionNarrow")
|
||||
return value.value.kind === "varRef" ? value.value.localId : null;
|
||||
if (
|
||||
value.kind !== "ternary" ||
|
||||
value.cond.kind !== "unionIsTag" ||
|
||||
value.cond.value.kind !== "varRef"
|
||||
)
|
||||
return null;
|
||||
const id = value.cond.value.localId;
|
||||
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
|
||||
return expr(value.else_) ? id : null;
|
||||
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
|
||||
return expr(value.then) ? id : null;
|
||||
return null;
|
||||
}
|
||||
const aliases: { alias: string; source: string }[] = [];
|
||||
const stringInput = (value: IrExpr): boolean => {
|
||||
if (value.kind === "varRef" && value.type.kind === "string") return true;
|
||||
@@ -145,6 +193,12 @@ function collectUses(
|
||||
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// The payload may escape; the union itself does not.
|
||||
if (node.value.kind === "varRef") {
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "unionIsTag":
|
||||
if (node.value.kind === "varRef") return true;
|
||||
break;
|
||||
@@ -161,12 +215,16 @@ function collectUses(
|
||||
node.left.kind === "varRef" &&
|
||||
node.left.type.kind === "union" &&
|
||||
node.type.kind !== "union"
|
||||
)
|
||||
) {
|
||||
uses.payloadKept.add(node.left.localId);
|
||||
return expr(node.right);
|
||||
}
|
||||
break;
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
if (node.obj.kind === "varRef") return true;
|
||||
// A field read of a binding, or of a union binding's narrowed
|
||||
// payload, keeps neither.
|
||||
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
// A copied record converts field by field; a live capsule keeps it.
|
||||
@@ -200,13 +258,19 @@ function collectUses(
|
||||
return node.args.every(expr);
|
||||
case "call":
|
||||
node.args.forEach((arg, index) => {
|
||||
if (arg.kind !== "varRef") {
|
||||
const narrowed = narrowedBinding(arg);
|
||||
if (narrowed !== null) {
|
||||
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
|
||||
return;
|
||||
}
|
||||
const wrapped =
|
||||
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
|
||||
const value = wrapped ? arg.value : arg;
|
||||
if (value.kind !== "varRef") {
|
||||
expr(arg);
|
||||
return;
|
||||
}
|
||||
let forwards = uses.forwards.get(arg.localId);
|
||||
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
|
||||
forwards.push({ callee: node.callee, index });
|
||||
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
|
||||
});
|
||||
return true;
|
||||
case "assignExpr":
|
||||
@@ -250,8 +314,11 @@ function collectUses(
|
||||
node.value.kind === "varRef" &&
|
||||
node.value.type.kind === "union" &&
|
||||
nullableField(node.className, node.field)
|
||||
)
|
||||
) {
|
||||
// The field retains the payload.
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return expr(node.obj);
|
||||
}
|
||||
const stored = fieldStore(
|
||||
node.obj,
|
||||
node.value,
|
||||
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
|
||||
registerReturns: ReadonlySet<string> = new Set(),
|
||||
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
|
||||
recordDynCopy: RecordDynCopyTest = () => false,
|
||||
pointerWrap: PointerWrapTest = () => false,
|
||||
): CallLifetimes {
|
||||
const usesByFunction = new Map<string, Uses>();
|
||||
const lazyCaptures = new LazyCaptures(functions);
|
||||
const nodes = new Map<string, Parameter[]>();
|
||||
/** Union parameters that keep their payload in place too, which a
|
||||
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
|
||||
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
|
||||
const unsafe: Parameter[] = [];
|
||||
const consumed: { fn: string; index: number }[] = [];
|
||||
const result: CallLifetimes = {
|
||||
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
|
||||
};
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.generator !== undefined && fn.classCaptures === undefined) {
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
const stable = new Set<string>();
|
||||
for (const param of fn.params) {
|
||||
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
|
||||
registerReturns.has(fn.name),
|
||||
inlineField,
|
||||
recordDynCopy,
|
||||
pointerWrap,
|
||||
);
|
||||
usesByFunction.set(fn.name, uses);
|
||||
// Class-capturing bodies get only local facts (computed below from
|
||||
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
payloadNodes.set(
|
||||
fn.name,
|
||||
fn.params.map((param) => {
|
||||
if (param.type.kind !== "union") return undefined;
|
||||
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
|
||||
if (!node.safe) unsafe.push(node);
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
}
|
||||
/** The fact a forwarded use needs from the callee's parameter. */
|
||||
const target = (use: ForwardedUse): Parameter | undefined =>
|
||||
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
|
||||
for (const fn of functions.values()) {
|
||||
const params = nodes.get(fn.name);
|
||||
const uses = usesByFunction.get(fn.name);
|
||||
if (!params || !uses) continue;
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
|
||||
if (!node.safe) return;
|
||||
for (const use of uses.forwards.get(param.localId) ?? []) {
|
||||
const callee = nodes.get(use.callee)?.[use.index];
|
||||
for (const callee of callees) {
|
||||
if (!callee) {
|
||||
node.safe = false;
|
||||
unsafe.push(node);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
callee.callers.add(node);
|
||||
}
|
||||
};
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const forwards = uses.forwards.get(param.localId) ?? [];
|
||||
depend(node, forwards.map(target));
|
||||
const payload = payloadNodes.get(fn.name)?.[index];
|
||||
if (!payload) return;
|
||||
// The payload stays in place when the union does, when every callee
|
||||
// the union reaches keeps its payload too, and when every callee its
|
||||
// narrowed payload reaches proves that parameter.
|
||||
depend(payload, [
|
||||
node,
|
||||
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
|
||||
...(uses.payloadForwards.get(param.localId) ?? []).map(
|
||||
(use) => nodes.get(use.callee)?.[use.index],
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < unsafe.length; i++) {
|
||||
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
|
||||
)
|
||||
continue;
|
||||
const forwards = uses.forwards.get(local.id) ?? [];
|
||||
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
|
||||
if (forwards.every((use) => target(use)?.safe === true)) {
|
||||
projected.add(local.id);
|
||||
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
|
||||
}
|
||||
|
||||
@@ -1410,18 +1410,18 @@ export function emitClassShapes(
|
||||
* statics plus their construct thunks — allocate, run the constructor over
|
||||
* a +1 `this`, and hand the remaining +1 out. No pending check is needed:
|
||||
* in-tier constructors are throw-free by construction.
|
||||
* `classObjs` maps className → { nameSym } registered during body emission
|
||||
* (the literal interned while the table was open). */
|
||||
* `classObjs` maps className → { nameSym, baseNameSym } registered during
|
||||
* body emission (the literals interned while the table was open). */
|
||||
export function emitClassObjDefs(
|
||||
host: ClassHost,
|
||||
metaMap: Map<string, LlClassMeta>,
|
||||
classObjs: Map<string, { nameSym: string }>,
|
||||
classObjs: Map<string, { nameSym: string; baseNameSym: string | null }>,
|
||||
fnByName: Map<string, IrFunction>,
|
||||
llType: (t: IrType) => string,
|
||||
threadLocal = false,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
for (const [className, { nameSym }] of classObjs) {
|
||||
for (const [className, { nameSym, baseNameSym }] of classObjs) {
|
||||
const meta = metaMap.get(className);
|
||||
if (!meta)
|
||||
throw new InternalCompilerError(
|
||||
@@ -1447,7 +1447,7 @@ export function emitClassObjDefs(
|
||||
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
|
||||
const classObj =
|
||||
`@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` +
|
||||
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`;
|
||||
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null, ptr ${baseNameSym ?? "null"}, ${host.sizeType} ${meta.def.ownStaticFields ? 1 : 0} } ; class ${className}`;
|
||||
if (meta.def.runtime) {
|
||||
// A runtime-provided class: %C.construct allocates through the
|
||||
// runtime and returns the instance.
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js";
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
|
||||
/** The caller has proved or guarded the direction's finite endpoint. Clamp
|
||||
* the opposite infinity before integer conversion. Fractional limits become
|
||||
@@ -50,8 +51,8 @@ export function emitCountedLoopLimit(
|
||||
}
|
||||
|
||||
/** The run-time test of a guarded counted loop: an unproven limit is within
|
||||
* the integer form's finite range and an unproven start is an exact safe
|
||||
* integer. Returns `true` when nothing needs testing. */
|
||||
* the integer form's finite range and an unproven start is an exact integer
|
||||
* within the loop's start bound. Returns `true` when nothing needs testing. */
|
||||
export function emitCountedLoopSafety(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
@@ -65,7 +66,11 @@ export function emitCountedLoopSafety(
|
||||
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
|
||||
if (loop.startGuarded) {
|
||||
const start = host.emitExpr(loop.start);
|
||||
const bound = loop.startBound;
|
||||
safe = andSafe(host, safe, emitExactIntegerTest(host, start.name, -bound, bound).ok);
|
||||
}
|
||||
for (const follower of loop.followers) {
|
||||
if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start));
|
||||
if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower));
|
||||
@@ -117,6 +122,7 @@ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPair
|
||||
}
|
||||
|
||||
function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
|
||||
if (ok === "true") return safe;
|
||||
if (safe === "true") return ok;
|
||||
const both = host.B.tmp();
|
||||
host.B.line(`${both} = and i1 ${safe}, ${ok}`);
|
||||
@@ -125,10 +131,25 @@ function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
|
||||
|
||||
/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit
|
||||
* patterns of the value and its round trip agree). NaN and infinities fail
|
||||
* the ordered range tests. */
|
||||
* the ordered range tests. A start with an integer form (an int32 local,
|
||||
* `p0 - 1` of one) is an exact integer already: its range answers the test,
|
||||
* or two integer compares do. */
|
||||
function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string {
|
||||
const B = host.B;
|
||||
const start = host.emitExpr(expr);
|
||||
const integer = exactInteger(host, start, expr);
|
||||
if (integer) {
|
||||
const limit = Number.MAX_SAFE_INTEGER;
|
||||
if (integer.range.min >= -limit && integer.range.max <= limit) return "true";
|
||||
const wide = widenInteger(host, integer),
|
||||
low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = icmp sge i64 ${wide}, ${-limit}`);
|
||||
B.line(`${high} = icmp sle i64 ${wide}, ${limit}`);
|
||||
B.line(`${ok} = and i1 ${low}, ${high}`);
|
||||
return ok;
|
||||
}
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
@@ -184,3 +205,39 @@ export function emitExactIntegerTest(
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
return { ok, integer };
|
||||
}
|
||||
|
||||
/** The i64 start of an integer counter: the integer form the start value
|
||||
* carries (an int32 local, `p0 - 1` of one), or the double's conversion,
|
||||
* which the loop's guard or proof makes exact. */
|
||||
export function emitCountedStart(host: LlvmEmitterContext, expr: IrExpr): string {
|
||||
const value = host.emitExpr(expr);
|
||||
const integer = exactInteger(host, value, expr);
|
||||
if (integer) return widenInteger(host, integer);
|
||||
const start = host.B.tmp();
|
||||
host.B.line(`${start} = fptosi double ${value.name} to i64`);
|
||||
return start;
|
||||
}
|
||||
|
||||
/** emitExactIntegerTest of a start expression: when the value carries an
|
||||
* integer form (an exact integer, never -0), its range answers the test or
|
||||
* two integer compares do, and the integer is the form's. */
|
||||
export function emitStartIntegerTest(
|
||||
host: LlvmEmitterContext,
|
||||
expr: IrExpr,
|
||||
min: number,
|
||||
max: number,
|
||||
): { ok: string; integer: string } {
|
||||
const value = host.emitExpr(expr);
|
||||
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 };
|
||||
const B = host.B;
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = icmp sge i64 ${wide}, ${min}`);
|
||||
B.line(`${high} = icmp sle i64 ${wide}, ${max}`);
|
||||
B.line(`${ok} = and i1 ${low}, ${high}`);
|
||||
return { ok, integer: wide };
|
||||
}
|
||||
|
||||
@@ -13,9 +13,37 @@ import {
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations. */
|
||||
/** A request a range emitter made of its debug info that added nodes
|
||||
* (LlvmDebugInfo.record), with the ids it answered. */
|
||||
export type DebugRequest =
|
||||
| { readonly op: "function"; readonly fn: string; readonly result: string }
|
||||
| {
|
||||
readonly op: "local";
|
||||
readonly local: IrLocal;
|
||||
readonly fn: string;
|
||||
readonly arg: number;
|
||||
readonly captured: boolean;
|
||||
readonly result: { variable: string; location: string };
|
||||
}
|
||||
| {
|
||||
readonly op: "location";
|
||||
readonly loc: SrcLoc;
|
||||
readonly fn: string;
|
||||
readonly result: string;
|
||||
};
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations.
|
||||
*
|
||||
* Nodes are numbered in request order. A range emitter (emit-module.ts)
|
||||
* records the requests that added nodes; the emitter that merges the range
|
||||
* replays them in order, so every node gets the number serial emission
|
||||
* gives it, and renames the range's text from the ids the range answered to
|
||||
* its own. A request that added no node answered an id that existed before
|
||||
* the range started or that an earlier recorded request created. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
/** The requests that added nodes, while recording (record). */
|
||||
private requests: DebugRequest[] | null = null;
|
||||
private readonly files = new Map<string, string>();
|
||||
private readonly locations = new Map<string, string>();
|
||||
private readonly scopes = new Map<string, string>();
|
||||
@@ -73,7 +101,53 @@ export class LlvmDebugInfo {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/** Records the requests that add nodes from here on. */
|
||||
record(): void {
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
/** The requests recorded since record. */
|
||||
recorded(): readonly DebugRequest[] {
|
||||
return this.requests ?? [];
|
||||
}
|
||||
|
||||
/** Replays a range's recorded requests in order and adds to `ids` the
|
||||
* rename from the ids the range answered to this debug info's. */
|
||||
replay(
|
||||
requests: readonly DebugRequest[],
|
||||
functionNamed: (name: string) => IrFunction,
|
||||
ids: Map<string, string>,
|
||||
): void {
|
||||
const own = (id: string, to: string | null | undefined): void => {
|
||||
if (to === null || to === undefined)
|
||||
throw new Error(`llvm emitter bug: debug request for ${id} added no node on replay`);
|
||||
if (to !== id) ids.set(id, to);
|
||||
};
|
||||
for (const request of requests) {
|
||||
if (request.op === "function") {
|
||||
own(request.result, this.function(functionNamed(request.fn)));
|
||||
continue;
|
||||
}
|
||||
const fn = ids.get(request.fn) ?? request.fn;
|
||||
if (request.op === "location") {
|
||||
own(request.result, this.location(request.loc, fn));
|
||||
continue;
|
||||
}
|
||||
const local = this.local(request.local, fn, request.arg, request.captured);
|
||||
own(request.result.variable, local?.variable);
|
||||
own(request.result.location, local?.location);
|
||||
}
|
||||
}
|
||||
|
||||
function(fn: IrFunction): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.functionNode(fn);
|
||||
if (this.requests !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "function", fn: fn.name, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private functionNode(fn: IrFunction): string | null {
|
||||
const pos = this.source.position(fn.loc);
|
||||
if (pos === null) return null;
|
||||
const file = this.file(pos.file);
|
||||
@@ -230,6 +304,26 @@ export class LlvmDebugInfo {
|
||||
fn: string | null,
|
||||
arg = 0,
|
||||
captured = false,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
const before = this.nodes.length;
|
||||
const result = this.localNodes(local, fn, arg, captured);
|
||||
if (this.requests !== null && fn !== null && result !== null && this.nodes.length > before)
|
||||
this.requests.push({
|
||||
op: "local",
|
||||
local,
|
||||
fn,
|
||||
arg,
|
||||
captured,
|
||||
result: { variable: result.variable, location: result.location },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private localNodes(
|
||||
local: IrLocal,
|
||||
fn: string | null,
|
||||
arg: number,
|
||||
captured: boolean,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
if (fn === null || local.source === undefined) return null;
|
||||
const pos = this.source.position(local.source.loc);
|
||||
@@ -279,6 +373,14 @@ export class LlvmDebugInfo {
|
||||
}
|
||||
|
||||
location(loc: SrcLoc, fn: string | null): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.locationNode(loc, fn);
|
||||
if (this.requests !== null && fn !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "location", loc, fn, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private locationNode(loc: SrcLoc, fn: string | null): string | null {
|
||||
if (fn === null) return null;
|
||||
const pos = this.source.position(loc);
|
||||
if (pos === null) return null;
|
||||
|
||||
@@ -122,6 +122,7 @@ const counted = (
|
||||
guarded: false,
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
startBound: Number.MAX_SAFE_INTEGER,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
|
||||
@@ -36,7 +36,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
|
||||
import { emitCountedLoopLimit, emitExactIntegerTest } from "./counted-loops.js";
|
||||
import { emitCountedLoopLimit, emitStartIntegerTest } from "./counted-loops.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
@@ -453,7 +453,7 @@ function emitPairedDenseGuard(
|
||||
): string {
|
||||
const B = host.B;
|
||||
const start = (expr: IrExpr, min: number, max: number) =>
|
||||
emitExactIntegerTest(host, host.emitExpr(expr).name, min, max);
|
||||
emitStartIntegerTest(host, expr, min, max);
|
||||
const low = start(pair.lowStart, 0, INT32_MAX - 1);
|
||||
const high = start(pair.highStart, -1, INT32_MAX - 1);
|
||||
let valid = B.tmp();
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
recordHandleCodecClass,
|
||||
textCodecRecordClass,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
@@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js
|
||||
import type { WalkerHost } from "./walkers.js";
|
||||
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
|
||||
* at any depth of arrays, Maps, Sets and promises: checked conversions to
|
||||
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
|
||||
* the two types share a typeKey. */
|
||||
function checkedConversionKey(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
|
||||
case "map":
|
||||
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
|
||||
case "set":
|
||||
return `set<${checkedConversionKey(t.elem)}>`;
|
||||
case "promise":
|
||||
return `promise<${checkedConversionKey(t.inner)}>`;
|
||||
default:
|
||||
return typeKey(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
|
||||
* and an event-listener record (a `handleEvent` function field) box their
|
||||
* identity. Every other shape, tuples included, copies its fields into a
|
||||
@@ -98,14 +117,16 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
|
||||
/** The handle tags of TextEncoder and TextDecoder records boxed as dyn
|
||||
* (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */
|
||||
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = {
|
||||
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder" | "StringDecoder", number> = {
|
||||
TextDecoder: 76,
|
||||
TextEncoder: 77,
|
||||
StringDecoder: 78,
|
||||
};
|
||||
|
||||
/** The decoder record fields scr_text_decoder_to_dyn reads, in its
|
||||
* parameter order. */
|
||||
const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const;
|
||||
const STRING_DECODER_FIELDS = ["%enc", "%pending"] as const;
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
@@ -151,6 +172,18 @@ export const DYN_KIND = {
|
||||
SYMBOL: 15,
|
||||
} as const;
|
||||
|
||||
/** Union arm kinds whose matchers reject every plain dyn object
|
||||
* (trustedArmByLiteral). */
|
||||
const NON_OBJECT_ARM_KINDS: ReadonlySet<IrType["kind"]> = new Set<IrType["kind"]>([
|
||||
"f64",
|
||||
"string",
|
||||
"bool",
|
||||
"nullT",
|
||||
"undefinedT",
|
||||
"bigint",
|
||||
"symbol",
|
||||
]);
|
||||
|
||||
/** What the dyn helpers need beyond the walker host: interned immortal
|
||||
* unit instances (undefined-armed dynCheck targets build them). */
|
||||
export interface DynHost extends WalkerHost {
|
||||
@@ -174,6 +207,10 @@ export interface DynHost extends WalkerHost {
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
/** The strict subclasses whose class-value constructor takes the same
|
||||
* arguments as `className`'s (classValueAbiEquals): the class values a
|
||||
* checked `classval` slot accepts besides its own class. */
|
||||
classValueSubtypes(className: string): readonly string[];
|
||||
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
||||
pendingTestLines(dest: string): string[];
|
||||
}
|
||||
@@ -193,6 +230,11 @@ function builtinStreamKind(className: string): number | null {
|
||||
return kinds[className] ?? null;
|
||||
}
|
||||
|
||||
/** Unions whose discriminant has fewer string literals keep one literal test
|
||||
* per arm (dispatchValues): one runtime lookup costs about what a few
|
||||
* literal tests do. */
|
||||
const DISCRIMINANT_DISPATCH_VALUES = 4;
|
||||
|
||||
export class LlDyn {
|
||||
private readonly dynMatchers = new Map<string, string>();
|
||||
private readonly dynBuilders = new Map<string, string>();
|
||||
@@ -383,6 +425,51 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
||||
}
|
||||
|
||||
/** Whether a builder of `def` may pick a plain object's arm by its literal
|
||||
* discriminator alone inside a trusted build (scr_dyn_trust_enter): the
|
||||
* arms' literals are disjoint, and every arm without a literal has a kind
|
||||
* no plain object matches. The value then matched exactly one arm with a
|
||||
* literal, the arm whose literal it carries. */
|
||||
private trustedArmByLiteral(def: IrUnionDef): boolean {
|
||||
const discriminant = def.discriminant;
|
||||
if (!discriminant) return false;
|
||||
const seen = new Set<string>();
|
||||
for (const { values } of discriminant.cases) {
|
||||
for (const value of values) {
|
||||
const key = `${typeof value}:${String(value)}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
return def.arms.every(
|
||||
(arm, tag) =>
|
||||
discriminant.cases.some((candidate) => candidate.tag === tag) ||
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind),
|
||||
);
|
||||
}
|
||||
|
||||
/** Per arm of `def`, the dyn kind of the values a builder may convert into
|
||||
* that arm without matching its structure inside a trusted build, or null.
|
||||
* A value there already matched one of the union's arms, so the arm is
|
||||
* known when it is the only arm that takes values of its kind: the arm
|
||||
* whose literal a plain object carries (trustedArmByLiteral), or the one
|
||||
* arm a plain object or an array can match at all. */
|
||||
private trustedArmKinds(def: IrUnionDef): (number | null)[] {
|
||||
const byLiteral = this.trustedArmByLiteral(def);
|
||||
const objectArms = def.arms.flatMap((arm, tag) =>
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag],
|
||||
);
|
||||
return def.arms.map((arm, tag) => {
|
||||
if (def.discriminant?.cases.some((candidate) => candidate.tag === tag))
|
||||
return byLiteral ? DYN_KIND.OBJ : null;
|
||||
if (objectArms.length !== 1 || objectArms[0] !== tag) return null;
|
||||
if (arm.kind === "array") return DYN_KIND.ARR;
|
||||
if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true)
|
||||
return DYN_KIND.OBJ;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** The order a dyn value tries a union's arms in: the canonical order,
|
||||
* except that among the record arms, those with more required fields go
|
||||
* first, then those with more fields. Records are width-tolerant, so an
|
||||
@@ -413,8 +500,123 @@ export class LlDyn {
|
||||
return order;
|
||||
}
|
||||
|
||||
/** The string literals of `def`'s discriminant when its arms are chosen
|
||||
* by reading the discriminant once (unionArmsByDiscriminant), or null
|
||||
* when the union keeps one literal test per arm: some literal is not a
|
||||
* string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */
|
||||
private dispatchValues(def: IrUnionDef): string[] | null {
|
||||
const values: string[] = [];
|
||||
for (const guard of def.discriminant?.cases ?? []) {
|
||||
if (guard.values.length === 0) return null;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string") return null;
|
||||
if (!values.includes(value)) values.push(value);
|
||||
}
|
||||
}
|
||||
return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values;
|
||||
}
|
||||
|
||||
/** Branches to `hits[tag]` for the first arm in `order` that `%d` matches
|
||||
* (unionArmMatch), or to `none` when no arm does, for
|
||||
* a union whose discriminant literals are `values` (dispatchValues). The
|
||||
* discriminant is read once (scr_dyn_discriminant_case, against the
|
||||
* constant `table`), and only the arms whose literal it holds and the arms
|
||||
* without a literal are tested, in `order`: every other arm's literal test
|
||||
* fails. An arm's failed check can run a getter that changes the
|
||||
* discriminant, so it is read again after each one; when its case
|
||||
* changed, the tests continue arm by arm after the failed arm, as they
|
||||
* would have run from the first arm. A value the runtime cannot classify
|
||||
* that way (a typed capsule, an accessor, an inherited discriminant) takes
|
||||
* the tests arm by arm. */
|
||||
private unionArmsByDiscriminant(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
values: readonly string[],
|
||||
order: readonly number[],
|
||||
hits: readonly string[],
|
||||
none: string,
|
||||
preserveRefs: boolean,
|
||||
table: string,
|
||||
trustedKinds: readonly (number | null)[] = def.arms.map(() => null),
|
||||
): void {
|
||||
const discriminant = def.discriminant!;
|
||||
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
||||
discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null;
|
||||
const host = this.host;
|
||||
this.defs.push(
|
||||
`${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`,
|
||||
``,
|
||||
);
|
||||
host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const classify = (): string => {
|
||||
const index = B.tmp();
|
||||
B.line(
|
||||
`${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`,
|
||||
);
|
||||
return index;
|
||||
};
|
||||
// The arm-by-arm tests, entered at any arm (`from[i]` tests order[i]
|
||||
// and the arms after it).
|
||||
const tests = B.newLabel("du.tests");
|
||||
const from = order.map((_, i) => (i === 0 ? tests : B.newLabel("du.t")));
|
||||
const after = (position: number): string =>
|
||||
position + 1 < order.length ? from[position + 1]! : none;
|
||||
// Tests the arms a value in case `k` (-1: no literal) can match.
|
||||
const tryCase = (k: number): void => {
|
||||
const value = k < 0 ? null : values[k]!;
|
||||
const positions = order
|
||||
.map((_, position) => position)
|
||||
.filter((position) => {
|
||||
const literals = literalsOf(order[position]!);
|
||||
return literals === null || (value !== null && literals.includes(value));
|
||||
});
|
||||
if (positions.length === 0) {
|
||||
B.br(none);
|
||||
return;
|
||||
}
|
||||
positions.forEach((position, index) => {
|
||||
const tag = order[position]!;
|
||||
const failed = B.newLabel("du.c.f");
|
||||
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs, trustedKinds[tag]!);
|
||||
B.startBlock(failed);
|
||||
const next = index === positions.length - 1 ? none : B.newLabel("du.c.n");
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${classify()}, ${k}`);
|
||||
B.condBr(same, next, after(position));
|
||||
if (next !== none) B.startBlock(next);
|
||||
});
|
||||
};
|
||||
const index = classify();
|
||||
const unmatched = B.newLabel("du.none");
|
||||
const cases = values.map(() => B.newLabel("du.case"));
|
||||
B.terminate(
|
||||
`switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`,
|
||||
);
|
||||
values.forEach((_, k) => {
|
||||
B.startBlock(cases[k]!);
|
||||
tryCase(k);
|
||||
});
|
||||
B.startBlock(unmatched);
|
||||
tryCase(-1);
|
||||
order.forEach((tag, position) => {
|
||||
B.startBlock(from[position]!);
|
||||
this.unionArmMatch(
|
||||
B,
|
||||
def,
|
||||
tag,
|
||||
hits[tag]!,
|
||||
after(position),
|
||||
preserveRefs,
|
||||
trustedKinds[tag]!,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
* structure match. Use the same path for predicates and builders. With a
|
||||
* `trustedKind` (builders, trustedArmKinds), a value of that dyn kind
|
||||
* inside a trusted build skips the structure match once its literal
|
||||
* matched. */
|
||||
private unionArmMatch(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
@@ -422,6 +624,7 @@ export class LlDyn {
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs = false,
|
||||
trustedKind: number | null = null,
|
||||
): void {
|
||||
const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag);
|
||||
if (guard && def.discriminant) {
|
||||
@@ -450,6 +653,34 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs, trustedKind);
|
||||
}
|
||||
|
||||
/** The part of unionArmMatch past the arm's literal test. With a
|
||||
* `trustedKind`, a value of that dyn kind inside a trusted build skips the
|
||||
* structure match. */
|
||||
private unionArmStructure(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
tag: number,
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs: boolean,
|
||||
trustedKind: number | null = null,
|
||||
): void {
|
||||
if (trustedKind !== null) {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`);
|
||||
const kind = this.kindOf(B, "%d");
|
||||
const sameKind = B.tmp();
|
||||
const trusted = B.tmp();
|
||||
const skip = B.tmp();
|
||||
B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`);
|
||||
B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`);
|
||||
B.line(`${skip} = and i1 ${sameKind}, ${trusted}`);
|
||||
const full = B.newLabel("du.full");
|
||||
B.condBr(skip, yes, full);
|
||||
B.startBlock(full);
|
||||
}
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -547,6 +778,13 @@ export class LlDyn {
|
||||
/* ── dyn node plumbing ─────────────────────────────────────────────── */
|
||||
|
||||
/** Loads a dyn node's kind tag (after the size_t rc word). */
|
||||
/** i1: the checked-dynamic value `d` is undefined. */
|
||||
isUndefined(B: BlockBuilder, d: string): string {
|
||||
const is = B.tmp();
|
||||
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
|
||||
return is;
|
||||
}
|
||||
|
||||
private kindOf(B: BlockBuilder, d: string): string {
|
||||
const p = B.tmp();
|
||||
const k = B.tmp();
|
||||
@@ -933,7 +1171,7 @@ export class LlDyn {
|
||||
|
||||
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
||||
dynMatchHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
const key = checkedConversionKey(t);
|
||||
const matcherKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynMatchers.get(matcherKey);
|
||||
if (existing) return existing;
|
||||
@@ -979,7 +1217,10 @@ export class LlDyn {
|
||||
const lPlain = B.newLabel("dm.tr.plain");
|
||||
B.condBr(capsule, lCapsule, lPlain);
|
||||
B.startBlock(lCapsule);
|
||||
if (preserveRefs) {
|
||||
// A class target matches only capsules of that class (tested above):
|
||||
// a materialized view is a plain object, which never carries a class
|
||||
// brand, so materializing one cannot change the answer.
|
||||
if (preserveRefs || (t.kind === "object" && t.className !== "%Error")) {
|
||||
B.terminate(`ret i1 false`);
|
||||
} else {
|
||||
const materialized = B.tmp();
|
||||
@@ -1245,7 +1486,7 @@ export class LlDyn {
|
||||
B.startBlock(l0);
|
||||
const n = this.lenOf(B, "%d");
|
||||
const items = this.itemsOf(B, "%d");
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext) => {
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
||||
@@ -1256,6 +1497,15 @@ export class LlDyn {
|
||||
brNext();
|
||||
B.startBlock(valueLabel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stored as T elements takes undefined
|
||||
// elements as well.
|
||||
const undef = B.tmp();
|
||||
const checkLabel = B.newLabel("dm.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, next, checkLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
|
||||
const lOk = B.newLabel("dm.eo");
|
||||
@@ -1275,11 +1525,26 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in canonical order; any full match answers true.
|
||||
const yes = B.newLabel("dm.y");
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const no = B.newLabel("dm.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
def.arms.map((_, tag) => tag),
|
||||
def.arms.map(() => yes),
|
||||
no,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
B.startBlock(no);
|
||||
} else
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
B.terminate(`ret i1 false`);
|
||||
B.startBlock(yes);
|
||||
B.terminate(`ret i1 true`);
|
||||
@@ -1336,8 +1601,9 @@ export class LlDyn {
|
||||
* fields, and at least one unless an index signature collects the
|
||||
* members), record members typed as unions of those leaves, records and
|
||||
* arrays, null and undefined (optional and nullable members; see
|
||||
* jsonUnionSupported), and index signatures whose value type is not a
|
||||
* union. Everything else keeps the checked-dynamic route alone. */
|
||||
* jsonUnionSupported), discriminated unions of records anywhere (see
|
||||
* jsonDiscriminatedSupported), and index signatures whose value type is
|
||||
* not a union. Everything else keeps the checked-dynamic route alone. */
|
||||
private jsonSchemaSupported(t: IrType, visiting: Set<string>, member = false): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
@@ -1364,13 +1630,42 @@ export class LlDyn {
|
||||
(!index || this.jsonSchemaSupported(index, visiting))
|
||||
);
|
||||
}
|
||||
case "union":
|
||||
case "union": {
|
||||
const def = this.host.unionsById.get(t.unionId);
|
||||
if (def?.discriminant) return this.jsonDiscriminatedSupported(def, visiting);
|
||||
return member && this.jsonUnionSupported(t, visiting);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A discriminated union the one-pass parser writes directly, as a record
|
||||
* member, an array element or the whole document: record arms that each
|
||||
* name the string literals their discriminant member holds, every
|
||||
* literal naming one arm, plus null and undefined arms. An object then
|
||||
* selects the one arm whose literal its discriminant member holds, the
|
||||
* only arm the checked route's literal tests leave to match
|
||||
* (unionArmsByDiscriminant); an object holding no such literal declines. */
|
||||
private jsonDiscriminatedSupported(def: IrUnionDef, visiting: Set<string>): boolean {
|
||||
const discriminant = def.discriminant!;
|
||||
const literals = new Set<string>();
|
||||
let records = 0;
|
||||
const supported = def.arms.every((arm, tag) => {
|
||||
if (arm.kind === "nullT" || arm.kind === "undefinedT") return true;
|
||||
if (arm.kind !== "record") return false;
|
||||
records++;
|
||||
const guard = discriminant.cases.find((candidate) => candidate.tag === tag);
|
||||
if (!guard || guard.values.length === 0) return false;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string" || literals.has(value)) return false;
|
||||
literals.add(value);
|
||||
}
|
||||
return this.jsonSchemaSupported(arm, visiting);
|
||||
});
|
||||
return supported && records >= 2;
|
||||
}
|
||||
|
||||
/** A union record member the one-pass parser writes directly: every arm
|
||||
* a number, boolean, string, null, undefined, or a supported record or
|
||||
* array, with at most one record and one array arm and no discriminant.
|
||||
@@ -1475,9 +1770,15 @@ export class LlDyn {
|
||||
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
|
||||
break;
|
||||
}
|
||||
case "union":
|
||||
body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
|
||||
case "union": {
|
||||
// A discriminated union can be a whole document, whose value the
|
||||
// parser releases when text follows it.
|
||||
const release = host.unionsById.get(t.unionId)?.discriminant
|
||||
? vAdapters(host, t).release
|
||||
: "null";
|
||||
body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr ${release}, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`);
|
||||
}
|
||||
@@ -1492,7 +1793,11 @@ export class LlDyn {
|
||||
/** The ScrJsonUnion descriptor of a union member (scr_json.h layout):
|
||||
* its slot representation, the arm each JSON value kind selects, and the
|
||||
* values the runtime stores for null and for a missing member. Boxed
|
||||
* unions build their boxes through `<name>_box`. */
|
||||
* unions build their boxes through `<name>_box`. A discriminated union
|
||||
* also lists its object arms by discriminant literal (`<name>_cases`),
|
||||
* and, because it can be an array element or a whole document, whose
|
||||
* values are boxes unless the union is a pointer union, it keeps its null
|
||||
* value and box whatever its field representation. */
|
||||
private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string {
|
||||
const host = this.host;
|
||||
const S = host.sizeType;
|
||||
@@ -1511,10 +1816,11 @@ export class LlDyn {
|
||||
);
|
||||
const nullTag = tags[0]!;
|
||||
const absentTag = tags[6]!;
|
||||
const nullValue = !inline && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
|
||||
const absentValue = !inline && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
|
||||
const values = !inline || def.discriminant !== undefined;
|
||||
const nullValue = values && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
|
||||
const absentValue = values && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
|
||||
let box = "null";
|
||||
if (repr === 2) {
|
||||
if (repr === 2 || (def.discriminant && !nullable)) {
|
||||
box = `@${name}_box`;
|
||||
const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`];
|
||||
const cases: string[] = [];
|
||||
@@ -1555,9 +1861,27 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
}
|
||||
let cases = "null";
|
||||
if (def.discriminant) {
|
||||
const discriminant = def.discriminant;
|
||||
const caseTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = discriminant.cases.flatMap((guard) =>
|
||||
guard.values.map(
|
||||
(value) =>
|
||||
`${caseTy} { ptr ${host.cstr(String(value))}, ${S} ${Buffer.byteLength(String(value), "utf8")}, ${S} ${guard.tag}, ptr @${this.jsonSchema(def.arms[guard.tag]!)} }`,
|
||||
),
|
||||
);
|
||||
const table = `@${name}_case_table`;
|
||||
cases = `@${name}_cases`;
|
||||
this.defs.push(
|
||||
`${table} = internal constant [${entries.length} x ${caseTy}] [${entries.join(", ")}]`,
|
||||
`${cases} = internal constant { ptr, ${S}, ${S}, ptr } { ptr ${host.cstr(discriminant.field)}, ${S} ${Buffer.byteLength(discriminant.field, "utf8")}, ${S} ${entries.length}, ptr ${table} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
const descriptor = `${name}_union`;
|
||||
this.defs.push(
|
||||
`@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box} }`,
|
||||
`@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box}, ptr ${cases} }`,
|
||||
``,
|
||||
);
|
||||
return descriptor;
|
||||
@@ -1607,7 +1931,12 @@ export class LlDyn {
|
||||
* dynCheck builder — which reports the exact error. Returns +1, or null
|
||||
* with the pending flag set. */
|
||||
jsonParseHelper(t: IrType): string | null {
|
||||
if (t.kind !== "record" && t.kind !== "array") return null;
|
||||
if (
|
||||
t.kind !== "record" &&
|
||||
t.kind !== "array" &&
|
||||
!(t.kind === "union" && this.host.unionsById.get(t.unionId)?.discriminant)
|
||||
)
|
||||
return null;
|
||||
const key = typeKey(t);
|
||||
const existing = this.jsonParsers.get(key);
|
||||
if (existing) return existing;
|
||||
@@ -1652,7 +1981,8 @@ export class LlDyn {
|
||||
* BUILD the typed value (+1), or throw the catchable path-annotated
|
||||
* TypeError and return a dummy with the pending flag set. */
|
||||
dynCheckHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
|
||||
const key = checkedConversionKey(t);
|
||||
const builderKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynBuilders.get(builderKey);
|
||||
if (existing) return existing;
|
||||
@@ -1670,6 +2000,26 @@ export class LlDyn {
|
||||
const B = new BlockBuilder();
|
||||
if (t.kind === "classval") {
|
||||
host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`);
|
||||
// A subclass's class value checks into the base class's slot when
|
||||
// construction through the slot can call its thunk with the base
|
||||
// constructor's arguments; any other subclass fails the check.
|
||||
const subclasses = host.classValueSubtypes(t.className);
|
||||
if (subclasses.length > 0) {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
|
||||
for (const className of subclasses) {
|
||||
const subKey = host.cstr(typeKey({ kind: "classval", className }));
|
||||
const matched = B.tmp();
|
||||
B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`);
|
||||
const yes = B.newLabel("sub.yes");
|
||||
const no = B.newLabel("sub.no");
|
||||
B.condBr(matched, yes, no);
|
||||
B.startBlock(yes);
|
||||
const sub = B.tmp();
|
||||
B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`);
|
||||
B.terminate(`ret ptr ${sub}`);
|
||||
B.startBlock(no);
|
||||
}
|
||||
}
|
||||
const checked = B.tmp();
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`,
|
||||
@@ -1687,11 +2037,18 @@ export class LlDyn {
|
||||
const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id));
|
||||
if (codec !== null) {
|
||||
// The record the codec's handle box holds (+1), or the path TypeError.
|
||||
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
||||
const checked = B.tmp();
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
if (codec === "StringDecoder") {
|
||||
host.declare(`declare ptr @scr_string_decoder_from_dyn(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_string_decoder_from_dyn(ptr %d, ptr %path, ptr ${want})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
|
||||
@@ -2432,6 +2789,21 @@ export class LlDyn {
|
||||
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// An undefined element is a present undefined element.
|
||||
const undef = B.tmp();
|
||||
const storeUndefined = B.newLabel("dca.undef");
|
||||
const checkLabel = B.newLabel("dca.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, storeUndefined, checkLabel);
|
||||
B.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
const at = B.tmp();
|
||||
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
||||
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
|
||||
this.pendingBail(
|
||||
@@ -2458,13 +2830,20 @@ export class LlDyn {
|
||||
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
||||
);
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
// builder can no longer fail, and it runs as a trusted build: a
|
||||
// nested union inside it picks the arm trustedArmKinds names without
|
||||
// matching the structure that this match already checked.
|
||||
const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def);
|
||||
const trustedCall = (call: () => void): void => {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`);
|
||||
host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`);
|
||||
const previous = B.tmp();
|
||||
B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`);
|
||||
call();
|
||||
B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`);
|
||||
};
|
||||
const build = (i: number): void => {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
// A matched unit arm builds nothing: THE interned immortal
|
||||
// instance (rc == SIZE_MAX — no retain owed).
|
||||
@@ -2490,21 +2869,58 @@ export class LlDyn {
|
||||
} else if (host.nullableUnions.has(t.unionId)) {
|
||||
// A nullable union is its (+1) reference payload.
|
||||
const x = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.terminate(`ret ptr ${x}`);
|
||||
} else {
|
||||
const rc = vAdapters(host, arm);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
const x = B.tmp();
|
||||
const u = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.line(
|
||||
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
||||
);
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
};
|
||||
const order = this.unionArmOrder(def);
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const hits = def.arms.map(() => B.newLabel("dcu.h"));
|
||||
const unmatched = B.newLabel("dcu.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
order,
|
||||
hits,
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
trustedKinds,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
build(i);
|
||||
}
|
||||
B.startBlock(unmatched);
|
||||
} else
|
||||
for (const i of order) {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!);
|
||||
B.startBlock(lHit);
|
||||
build(i);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
@@ -2755,14 +3171,14 @@ export class LlDyn {
|
||||
return matched;
|
||||
}
|
||||
|
||||
/** A TextEncoder or TextDecoder record boxes as its handle (+1), which
|
||||
* holds the record by reference. A decoder passes the byte offsets of the
|
||||
* fields its boxed decode reads. */
|
||||
/** A TextEncoder, TextDecoder or StringDecoder record boxes as its
|
||||
* handle (+1), which holds the record by reference. The decoders pass
|
||||
* the byte offsets of the fields their boxed methods read. */
|
||||
private textCodecToDyn(
|
||||
B: BlockBuilder,
|
||||
t: IrType & { kind: "record" },
|
||||
shape: IrRecordShape,
|
||||
codec: "TextEncoder" | "TextDecoder",
|
||||
codec: "TextEncoder" | "TextDecoder" | "StringDecoder",
|
||||
): void {
|
||||
const host = this.host;
|
||||
const rc = vAdapters(host, t);
|
||||
@@ -2777,10 +3193,12 @@ export class LlDyn {
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const start = B.tmp();
|
||||
B.line(`${start} = ptrtoint ptr %v to ${this.S}`);
|
||||
const offsets = TEXT_DECODER_FIELDS.map((field) => {
|
||||
const fields: readonly string[] =
|
||||
codec === "StringDecoder" ? STRING_DECODER_FIELDS : TEXT_DECODER_FIELDS;
|
||||
const offsets = fields.map((field) => {
|
||||
const index = shape.fields.findIndex((f) => f.name === field);
|
||||
if (index < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`);
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${codec} record lacks ${field}`);
|
||||
const p = B.tmp();
|
||||
const at = B.tmp();
|
||||
const offset = B.tmp();
|
||||
@@ -2791,11 +3209,11 @@ export class LlDyn {
|
||||
B.line(`${offset} = sub ${this.S} ${at}, ${start}`);
|
||||
return `${this.S} ${offset}`;
|
||||
});
|
||||
host.declare(
|
||||
`declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`,
|
||||
);
|
||||
const fn =
|
||||
codec === "StringDecoder" ? "scr_string_decoder_to_dyn" : "scr_text_decoder_to_dyn";
|
||||
host.declare(`declare ptr @${fn}(ptr, ptr, ptr, ${fields.map(() => this.S).join(", ")})`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
||||
`${r} = call ptr @${fn}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
@@ -2975,7 +3393,7 @@ export class LlDyn {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const codec = recordTextCodecClass(shape);
|
||||
const codec = recordHandleCodecClass(shape);
|
||||
if (codec !== null) {
|
||||
this.textCodecToDyn(B, t, shape, codec);
|
||||
break;
|
||||
|
||||
@@ -20,7 +20,12 @@ import { constructorClass } from "../../ir/final-fields.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js";
|
||||
import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js";
|
||||
import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js";
|
||||
import {
|
||||
findMapReadBorrows,
|
||||
findMapReadLifetimes,
|
||||
findReadNumberMapWrites,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { borrowableMapGet } from "./expr-containers.js";
|
||||
import { findMapEntryFusions } from "./map-entry-fusion.js";
|
||||
import { findLocalStackUnions } from "./stack-unions.js";
|
||||
import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js";
|
||||
@@ -56,12 +61,17 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name);
|
||||
emitter.usesLandingPads = false;
|
||||
emitter.aliasScopedVersions = 0;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.speculativeLeaves = 0;
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
@@ -136,6 +146,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
@@ -159,6 +170,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.optionalArrayReads,
|
||||
emitter.referenceWrites,
|
||||
emitter.fnByName,
|
||||
emitter.nodeIndex,
|
||||
)
|
||||
: new Set();
|
||||
emitter.localArrayReads = findLocalArrayReads(
|
||||
@@ -186,15 +198,27 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
fn,
|
||||
emitter.callLifetimes,
|
||||
emitter.scalarStringSlices,
|
||||
emitter.lentStringParameters,
|
||||
emitter.lendingFacts,
|
||||
)
|
||||
: new Set();
|
||||
emitter.mapReadLifetimes = findMapReadLifetimes(
|
||||
fn,
|
||||
emitter.boxedUnionsById,
|
||||
emitter.callLifetimes,
|
||||
emitter.referenceEffects,
|
||||
);
|
||||
const stable = emitter.callLifetimes.bindings.get(fn.name);
|
||||
emitter.borrowedMapGets = findMapReadBorrows(
|
||||
fn,
|
||||
emitter.referenceEffects,
|
||||
(decl) =>
|
||||
decl.init !== null &&
|
||||
stable?.has(decl.localId) === true &&
|
||||
!emitter.mapReadLifetimes.locals.has(decl.localId) &&
|
||||
borrowableMapGet(emitter, decl.init),
|
||||
);
|
||||
emitter.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
emitter.numberMapKeyIntegers = new Map();
|
||||
emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null;
|
||||
emitter.localStackUnions = findLocalStackUnions(
|
||||
fn,
|
||||
@@ -324,7 +348,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
|
||||
const paramIds = new Set(fn.params.map((p) => p.localId));
|
||||
if (emitter.debug === null) {
|
||||
for (const id of findIntegerViews(numericFn)) {
|
||||
for (const id of findIntegerViews(numericFn, emitter.nodeIndex)) {
|
||||
// An i32 slot is its own view.
|
||||
if (emitter.int32Locals.has(id)) continue;
|
||||
const slot = B.slot();
|
||||
@@ -595,6 +619,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
B.terminate(cleanup.terminator);
|
||||
}
|
||||
|
||||
if (emitter.usesLandingPads && emitter.currentUnwindAbi)
|
||||
B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`);
|
||||
if (emitter.logArgSlots > 0) {
|
||||
B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`);
|
||||
}
|
||||
@@ -635,7 +661,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`;
|
||||
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
|
||||
emitter.privateGlobalSlots.clear();
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
// Landing pads (emitDirectCall) need the personality.
|
||||
const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi);
|
||||
if (emitter.integerClones.members.has(fn.name)) {
|
||||
body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true);
|
||||
@@ -1307,8 +1336,10 @@ export function emitArgPackAndTrampolinePrologue(
|
||||
* code. */
|
||||
export function parkingFunctions(emitter: LlEmitter): Set<string> | null {
|
||||
if (emitter.wasi || emitter.mod.workers === true) return null;
|
||||
for (const decl of emitter.decls) if (/@scr_test_sub\b/.test(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null;
|
||||
const testSub = (text: string): boolean =>
|
||||
text.includes("@scr_test_sub") && /@scr_test_sub\b/.test(text);
|
||||
for (const decl of emitter.decls) if (testSub(decl)) return null;
|
||||
for (const text of emitter.fnDefText.values()) if (testSub(text)) return null;
|
||||
// Reverse mention edges, then propagate "may park" to every mentioner.
|
||||
const mentionedBy = new Map<string, Set<string>>();
|
||||
const parks = new Set<string>();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* replay calls the same entry point with the same arguments, so every
|
||||
* helper gets the number and the definition the serial emission gives it,
|
||||
* and the range's text is renamed to those numbers. */
|
||||
import type { DebugRequest } from "./debug-info.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** One event of a range's emission, in order. */
|
||||
@@ -97,6 +98,8 @@ export class EmitJournal {
|
||||
* worker process can write it. */
|
||||
export interface ChunkResult {
|
||||
readonly entries: readonly JournalEntry[];
|
||||
/** The requests that added debug metadata nodes (LlvmDebugInfo.record). */
|
||||
readonly debugRequests: readonly DebugRequest[];
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
@@ -152,23 +155,83 @@ export function replayJournal(
|
||||
}
|
||||
|
||||
/** The rename of a range's text: numbered names (`names`, without the
|
||||
* sigil) and field alias tags (`tags`). One scan for each kind of name:
|
||||
* a numbered name starts `sc_` after a character that cannot be part of a
|
||||
* name, and ends at the last digit after its counter prefix. */
|
||||
* sigil), field alias tags (`tags`) and debug metadata ids (`debugIds`,
|
||||
* `!N`). One scan for each kind of name: a numbered name starts `sc_`
|
||||
* after a character that cannot be part of a name, and ends at the last
|
||||
* digit after its counter prefix. */
|
||||
export function renameChunk(
|
||||
names: ReadonlyMap<string, string>,
|
||||
tags: ReadonlyMap<number, number>,
|
||||
debugIds: ReadonlyMap<string, string> = new Map(),
|
||||
): (text: string) => string {
|
||||
const prefixes = new Set<string>();
|
||||
for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, ""));
|
||||
if (prefixes.size === 0 && tags.size === 0) return (text) => text;
|
||||
if (prefixes.size === 0 && tags.size === 0 && debugIds.size === 0) return (text) => text;
|
||||
return (text) => {
|
||||
let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes);
|
||||
if (tags.size > 0) out = renameTags(out, tags);
|
||||
if (debugIds.size > 0) out = renameDebugIds(out, debugIds);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/** Renames metadata references `!N` outside string literals and comments.
|
||||
* Debug builds attach no alias tags, so every such reference in a
|
||||
* function's text is a debug metadata id. A string literal or a comment
|
||||
* (`;` outside a string literal) ends at the end of its line; a quote
|
||||
* inside a string literal is escaped (`\22`). */
|
||||
function renameDebugIds(text: string, ids: ReadonlyMap<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
// The line of the reference being looked at, and whether the reference
|
||||
// is inside a string literal or a comment there. The next quote and
|
||||
// semicolon positions only move forward, so each is found once.
|
||||
let lineEnd = -1;
|
||||
let quoted = false;
|
||||
let comment = false;
|
||||
let quote = text.indexOf('"');
|
||||
let semicolon = text.indexOf(";");
|
||||
for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) {
|
||||
if (at > lineEnd) {
|
||||
lineEnd = text.indexOf("\n", at);
|
||||
if (lineEnd < 0) lineEnd = text.length;
|
||||
quoted = false;
|
||||
comment = false;
|
||||
// Quotes and semicolons of earlier lines end with them.
|
||||
if ((quote >= 0 && quote < at) || (semicolon >= 0 && semicolon < at)) {
|
||||
const lineStart = text.lastIndexOf("\n", at) + 1;
|
||||
while (quote >= 0 && quote < lineStart) quote = text.indexOf('"', quote + 1);
|
||||
while (semicolon >= 0 && semicolon < lineStart)
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
while (!comment) {
|
||||
const nextQuote = quote >= 0 && quote < at ? quote : -1;
|
||||
const nextSemicolon = semicolon >= 0 && semicolon < at ? semicolon : -1;
|
||||
if (nextQuote < 0 && nextSemicolon < 0) break;
|
||||
if (nextQuote >= 0 && (nextSemicolon < 0 || nextQuote < nextSemicolon)) {
|
||||
quoted = !quoted;
|
||||
quote = text.indexOf('"', quote + 1);
|
||||
} else {
|
||||
if (!quoted) comment = true;
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
if (quoted || comment) continue;
|
||||
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
|
||||
let end = at + 1;
|
||||
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
|
||||
if (end === at + 1 || (end < text.length && isNameChar(text.charCodeAt(end)))) continue;
|
||||
const own = ids.get(text.slice(at, end));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, at), own);
|
||||
copied = end;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function renameNames(
|
||||
text: string,
|
||||
names: ReadonlyMap<string, string>,
|
||||
|
||||
@@ -61,10 +61,11 @@ import { hasLoneSurrogate, wtf8Bytes } from "../literals.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
|
||||
import { literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
@@ -101,10 +102,10 @@ function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
|
||||
return fnDefs;
|
||||
}
|
||||
|
||||
/** Modules with fewer functions emit in one process: forking and merging
|
||||
* would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS
|
||||
* lowers it, for tests). */
|
||||
function workerMinFunctions(): number {
|
||||
/** Modules with fewer functions emit in one process, and compute their
|
||||
* analyses in it: forking and merging would cost more than the workers save
|
||||
* (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */
|
||||
export function workerMinFunctions(): number {
|
||||
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
|
||||
return Number.isFinite(value) && value > 0 ? value : 2000;
|
||||
}
|
||||
@@ -133,6 +134,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] |
|
||||
let code = 1;
|
||||
try {
|
||||
emitter.journal = new EmitJournal();
|
||||
emitter.debug?.record();
|
||||
const texts: string[] = [];
|
||||
for (const fn of functions.slice(range * size, (range + 1) * size)) {
|
||||
texts.push(emitter.emitFunction(fn));
|
||||
@@ -206,16 +208,33 @@ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean)
|
||||
];
|
||||
}
|
||||
|
||||
/** The group line written before each function's text. Functions from one
|
||||
* source file share a group, which the LLVM helper keeps in the same
|
||||
* partitions so that their calls to each other stay inside a partition
|
||||
* (native/llvm-codegen/src/partition.h). A group is numbered by the first
|
||||
* function of its file in the module's function order, which does not depend
|
||||
* on the host. Functions without a source file get no group. */
|
||||
function partitionGroupLines(functions: readonly IrFunction[]): string[] {
|
||||
const groups = new Map<string, number>();
|
||||
return functions.map((fn) => {
|
||||
const file = fn.loc.file;
|
||||
if (file === "") return PARTITION_GROUP_END;
|
||||
let group = groups.get(file);
|
||||
if (group === undefined) groups.set(file, (group = groups.size));
|
||||
return `${PARTITION_GROUP_END} ${group}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Ends the group of the definitions that follow. */
|
||||
const PARTITION_GROUP_END = "; scriptc-group";
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
// Debug metadata is numbered per emitter and not journaled, so a debug
|
||||
// build emits every body here.
|
||||
const ranges = emitter.debug === null;
|
||||
const chunks = emitter.targetOptions.functionChunks ?? 1;
|
||||
const fnDefs =
|
||||
(ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
(ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ??
|
||||
(chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ??
|
||||
[];
|
||||
if (fnDefs.length === 0)
|
||||
for (const fn of emitter.mod.functions) {
|
||||
@@ -344,6 +363,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Fetch-referencing programs register native fetch before main.
|
||||
// Runtime selection compiles scr_fetch.c on the same predicate.
|
||||
const usesFetch = runtimeFeatures.fetch;
|
||||
// URL programs give the host parser the IDNA unit's ToASCII.
|
||||
const usesUrlIdna = runtimeFeatures.urlIdna;
|
||||
const snapshotsTlsCa = runtimeFeatures.tls || runtimeFeatures.tlsCa;
|
||||
// The process verdict has the same precedence as the C reference
|
||||
// emitter: node:test owns the final status when present; otherwise an
|
||||
@@ -396,6 +417,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
if (usesHttp2) emitter.declare(`declare void @scr_http2_dyn_install()`);
|
||||
if (usesHttp) emitter.declare(`declare void @scr_http_dyn_install()`);
|
||||
if (usesFetch) emitter.declare(`declare void @scr_fetch_install()`);
|
||||
if (usesUrlIdna) emitter.declare(`declare void @scr_url_idna_install()`);
|
||||
|
||||
if (usesNodeTest) {
|
||||
emitter.declare(`declare i32 @scr_test_exit_code()`);
|
||||
@@ -646,7 +668,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// class-object shape { rc, pre, post, ctor, name } — field reads on
|
||||
// builtin errors and classval loads GEP through these.
|
||||
`%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`,
|
||||
`%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`,
|
||||
`%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ptr, ${emitter.sizeType} }`,
|
||||
// The runtime emitter prefix { rc, vt, reg, cls } — user subclasses
|
||||
// embed it (classes.ts), and bare-emitter GEPs address through it.
|
||||
`%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`,
|
||||
@@ -912,9 +934,13 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of emitter.resolveThunkDefs) out.push(line);
|
||||
out.push(fnDefs[0] ?? "");
|
||||
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
|
||||
out.push("");
|
||||
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]!);
|
||||
}
|
||||
if (fnDefs.length === 0) out.push("");
|
||||
out.push(PARTITION_GROUP_END, "");
|
||||
// WebAssembly modules translated into this program, and the registry.
|
||||
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
|
||||
out.push(line);
|
||||
@@ -986,7 +1012,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// LIBRARY mode: no @main — the profile-declared external
|
||||
// symbols specified by the library IR instead.
|
||||
for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line);
|
||||
out.push(...emitter.inlineRcTail(flushedDecls));
|
||||
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
|
||||
out.push(...functionAttributes(emitter, hasNoInlineRecordClone));
|
||||
out.push(``);
|
||||
out.push(...emitter.fieldAliasMetadata());
|
||||
@@ -1027,6 +1053,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
...(emitter.ffiHasForeignCallback ? [` call void @scr_ffi_install()`] : []),
|
||||
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
|
||||
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
|
||||
...(usesUrlIdna ? [` call void @scr_url_idna_install()`] : []),
|
||||
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
|
||||
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
|
||||
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
|
||||
@@ -1131,7 +1158,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
: [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]),
|
||||
`}`,
|
||||
``,
|
||||
...emitter.inlineRcTail(flushedDecls),
|
||||
);
|
||||
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
|
||||
out.push(
|
||||
// sanitize_address is inert under the plain pipeline; the sanitized
|
||||
// lane's -fsanitize=address link activates instrumentation over the
|
||||
// emitted functions too (the runtime TUs get theirs from clang).
|
||||
@@ -1270,6 +1299,42 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word, its
|
||||
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
|
||||
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
|
||||
@@ -1586,9 +1651,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
|
||||
// storage, or, in worker executables, from the thread's static TLS block,
|
||||
// one thread-pointer-relative load (see executableTls).
|
||||
const scanInline =
|
||||
emitter.executableTls ||
|
||||
(emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true);
|
||||
const scanInline = stringScanInline(emitter);
|
||||
let scanDeclared = false;
|
||||
for (const helper of STRING_READ_HELPERS) {
|
||||
if (!emitter.decls.has(helper.decl)) continue;
|
||||
@@ -1612,8 +1675,8 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// order; other differing words decide by their first differing byte,
|
||||
// and strings shorter than a word by their first byte. Otherwise a
|
||||
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
|
||||
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
|
||||
// strings, so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
|
||||
// so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// only test the sign of the result.
|
||||
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
|
||||
const fn = "scr_str_cmp_u16";
|
||||
@@ -1743,7 +1806,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` %anyascii = icmp slt i32 %cws, 0`,
|
||||
` br i1 %anyascii, label %memorder, label %units`,
|
||||
`memorder:`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
` ret i32 %m`,
|
||||
`units:`,
|
||||
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
|
||||
@@ -32,7 +32,7 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js";
|
||||
import { boxAccess, boxNewCall, elemAccess } from "./shapes.js";
|
||||
import { dynHoleRead } from "./loop-array-borrows.js";
|
||||
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
|
||||
import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { charAtUnitRead, emitBorrowedMapGet, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
import {
|
||||
emitCountedLoopLimit,
|
||||
emitCountedLoopSafety,
|
||||
emitCountedStart,
|
||||
emitPairedLoopSafety,
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js";
|
||||
@@ -90,6 +91,7 @@ import {
|
||||
} from "./string-construction.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
import { loopAccessesTypedArrays } from "../loop-polls.js";
|
||||
|
||||
/** A callback loop version copies the loop; keep the copies small. */
|
||||
const MAX_CALLBACK_VERSION_COST = 400;
|
||||
@@ -104,6 +106,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
const b = emitter.binding(s.localId);
|
||||
if (
|
||||
emitter.borrowedMapGets.has(s.localId) &&
|
||||
b.kind === "local" &&
|
||||
s.init?.kind === "mapIntrinsic"
|
||||
) {
|
||||
// The map entry's own reference: no retain now, no release at scope
|
||||
// exit (findMapReadBorrows).
|
||||
const borrowed = emitBorrowedMapGet(emitter, s.init);
|
||||
if (borrowed) {
|
||||
B.line(`store ptr ${borrowed.name}, ptr ${b.slot}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (emitter.walkBorrows.has(s.localId) && b.kind === "local") {
|
||||
// A borrowed walk pointer: no retain now, no release at scope exit.
|
||||
const v = s.init === null ? "null" : emitter.emitReadReceiver(s.init).name;
|
||||
@@ -464,6 +479,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitter.publishedTypes?.containerGuarded(s.arr.type) !== true
|
||||
)
|
||||
break;
|
||||
// The literal it follows was constructed with this capacity
|
||||
// (reservedArrayLength).
|
||||
if (emitter.consumedReserves.delete(s)) break;
|
||||
const arr = emitBorrowedInput(emitter, s.arr);
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
@@ -570,6 +588,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.value,
|
||||
emitter.bytesBounds.has(s),
|
||||
emitter.bytesOwned.has(s),
|
||||
emitter.bytesAliasScopes.get(s),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -994,7 +1013,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
LOOPS.of(emitter).denseLoop = dense;
|
||||
@@ -1018,7 +1037,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).byteWindowsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.integerRanges = window.ranges;
|
||||
@@ -1072,7 +1091,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
B.br(countedJoin);
|
||||
B.startBlock(fast);
|
||||
@@ -1112,9 +1131,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.entryAllocas.push(
|
||||
`${integerSlot} = alloca i64 ; integer induction ${emitter.currentLocals.get(countedLoop.localId)!.name}`,
|
||||
);
|
||||
const startNumber = emitter.emitExpr(countedLoop.start);
|
||||
const start = B.tmp();
|
||||
B.line(`${start} = fptosi double ${startNumber.name} to i64`);
|
||||
const start = emitCountedStart(emitter, countedLoop.start);
|
||||
B.line(`store i64 ${start}, ptr ${integerSlot}`);
|
||||
emitter.integerLoopBindings.set(countedLoop.localId, {
|
||||
slot: integerSlot,
|
||||
@@ -1130,9 +1147,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.entryAllocas.push(
|
||||
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`,
|
||||
);
|
||||
const value = emitter.emitExpr(follower.start);
|
||||
const followerStart = B.tmp();
|
||||
B.line(`${followerStart} = fptosi double ${value.name} to i64`);
|
||||
const followerStart = emitCountedStart(emitter, follower.start);
|
||||
B.line(`store i64 ${followerStart}, ptr ${slot}`);
|
||||
followerSlots.push(slot);
|
||||
emitter.integerLoopBindings.set(follower.localId, {
|
||||
@@ -1149,9 +1164,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.entryAllocas.push(
|
||||
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`,
|
||||
);
|
||||
const value = emitter.emitExpr(counter.start);
|
||||
const start = B.tmp();
|
||||
B.line(`${start} = fptosi double ${value.name} to i64`);
|
||||
const start = emitCountedStart(emitter, counter.start);
|
||||
B.line(`store i64 ${start}, ptr ${slot}`);
|
||||
pairedSlots.push(slot);
|
||||
const dense = LOOPS.of(emitter).denseLoop;
|
||||
@@ -1199,9 +1212,68 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`store ptr ${fresh}, ptr ${slot}`);
|
||||
}
|
||||
};
|
||||
const workerBudget = emitter.workerLoopBudget(s);
|
||||
// A counted loop over typed arrays, or a loop to a typed array's
|
||||
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
|
||||
// of counting a budget in its body: the inner loop over a block then
|
||||
// has no exit besides its own bound, which LLVM can vectorize. A typed
|
||||
// array's length is fixed, so each block can take it as its limit when
|
||||
// the loop does not assign the array's binding.
|
||||
const stripMined =
|
||||
capturedLets.length === 0 &&
|
||||
integerSlot !== null &&
|
||||
((countedLoop !== null &&
|
||||
countedLimit !== null &&
|
||||
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
|
||||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
|
||||
emitter.workerLoopPolls(s) &&
|
||||
loopAccessesTypedArrays(s);
|
||||
const workerBudget = stripMined ? null : emitter.workerLoopBudget(s);
|
||||
// One slot for every copy of the loop: a speculative copy resumes an
|
||||
// iteration in another copy's body, which then reads this bound.
|
||||
let blockBound: string | null = null;
|
||||
if (stripMined) {
|
||||
blockBound = B.slot();
|
||||
B.entryAllocas.push(
|
||||
`${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`,
|
||||
);
|
||||
}
|
||||
freshenBindings();
|
||||
emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => {
|
||||
if (stripMined) {
|
||||
const counter: StripMinedCounter = countedLoop
|
||||
? {
|
||||
slot: integerSlot!,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
step: countedLoop.step,
|
||||
followers: countedLoop.followers.map((f, i) => ({
|
||||
slot: followerSlots[i]!,
|
||||
step: f.step,
|
||||
})),
|
||||
limit: () => countedLimit!,
|
||||
}
|
||||
: {
|
||||
slot: integerSlot!,
|
||||
type: emitter.sizeType,
|
||||
signed: false,
|
||||
step: 1,
|
||||
followers: [],
|
||||
limit: () => {
|
||||
const receiver = emitter.emitStableReceiver(integerLoop!.limitReceiver, []);
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(
|
||||
`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(
|
||||
`${len} = load ${emitter.sizeType}, ptr ${lenPtr}${bytesHeaderAlias(emitter)}`,
|
||||
);
|
||||
return len;
|
||||
},
|
||||
};
|
||||
emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit);
|
||||
return;
|
||||
}
|
||||
const lc = B.newLabel("loop.c");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu =
|
||||
@@ -2049,7 +2121,7 @@ function emitCallbackVersionedLoop(
|
||||
emitter.callbackVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
|
||||
@@ -2062,17 +2134,74 @@ function emitCallbackVersionedLoop(
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/** The copy of a versioned loop that runs when its guard fails. Its loops
|
||||
* get no speculative or int32 kind copies, which would emit each loop again
|
||||
* for a path that rarely runs. Proven integer loops, which emit one copy,
|
||||
* are kept. A typed-array version whose guard requires thread-owned
|
||||
* storage does not use it (emitVersionedLoop). */
|
||||
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
const copies = emitter.loopCopiesEnabled;
|
||||
emitter.loopCopiesEnabled = false;
|
||||
try {
|
||||
emitter.emitStmt(s);
|
||||
} finally {
|
||||
emitter.loopCopiesEnabled = copies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entries in a row whose fast copy bailed before a speculative loop site
|
||||
* stops trying it. */
|
||||
const SPECULATION_BAILS = 8;
|
||||
|
||||
/** The attachment of each scoped access in a version's fast copy: the
|
||||
* scope of its own view, and no alias with the scopes of the other views,
|
||||
* whose bytes the guard proved disjoint. The scopes are named after the
|
||||
* function and the version, so every copy of a loop has its own, and
|
||||
* written inline (string-named scopes need no numbered metadata). Also
|
||||
* the scopes, which each fast copy declares on entry (declareAliasScopes). */
|
||||
function bytesAliasScopes(
|
||||
emitter: LlEmitter,
|
||||
scopes: NonNullable<BytesLoopVersion["scopes"]>,
|
||||
): { attachments: Map<IrExpr | IrStmt, string>; scopes: string[] } {
|
||||
const version = emitter.aliasScopedVersions++;
|
||||
const name = emitter.currentFunctionName.replace(/[^A-Za-z0-9_.$%]/g, "_");
|
||||
const domain = '!{!"scriptc typed-array views"}';
|
||||
const scope = (receiver: number): string => `!{!"${name}.${version}.${receiver}", ${domain}}`;
|
||||
const attachments = new Map<number, string>();
|
||||
for (const receiver of scopes.receivers) {
|
||||
const others = scopes.receivers.filter((other) => other !== receiver).map(scope);
|
||||
attachments.set(
|
||||
receiver,
|
||||
`, !alias.scope !{${scope(receiver)}}, !noalias !{${others.join(", ")}}`,
|
||||
);
|
||||
}
|
||||
const result = new Map<IrExpr | IrStmt, string>();
|
||||
for (const [node, receiver] of scopes.nodes) result.set(node, attachments.get(receiver)!);
|
||||
return { attachments: result, scopes: scopes.receivers.map(scope) };
|
||||
}
|
||||
|
||||
/** Declares a version's scopes where a fast copy starts. The guard proves
|
||||
* the views disjoint for this entry only: an enclosing loop may pass other
|
||||
* views on its next pass (`[cur, next] = [next, cur]`). A declared scope
|
||||
* holds from its declaration, and LLVM gives each copy of the declaration
|
||||
* its own scopes when it unrolls or duplicates the enclosing code, so the
|
||||
* no-alias facts of one entry never apply to the accesses of another. */
|
||||
function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void {
|
||||
if (scopes.length === 0) return;
|
||||
emitter.declare("declare void @llvm.experimental.noalias.scope.decl(metadata)");
|
||||
for (const scope of scopes)
|
||||
emitter.B.line(`call void @llvm.experimental.noalias.scope.decl(metadata !{${scope}})`);
|
||||
}
|
||||
|
||||
/** Emit a loop's condition, body and update through `emitCopy`, twice
|
||||
* when speculative-integers.ts plans a fast integer copy. Both copies
|
||||
* share the loop's setup and exit; the ordinary copy comes first so the
|
||||
* fast copy is only emitted when every resume point exists. */
|
||||
/** A typed-array loop version: the guard, then the checked copy, which
|
||||
* runs whenever the guard fails, and the copy whose accesses the guard
|
||||
* proves in bounds (or thread-owned). */
|
||||
* proves in bounds (or thread-owned). In worker programs a version that
|
||||
* proves both also has a copy for views with shared storage, in bounds but
|
||||
* not owned. */
|
||||
function emitVersionedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" },
|
||||
@@ -2082,28 +2211,83 @@ function emitVersionedLoop(
|
||||
const fast = B.newLabel("bytes.version.fast"),
|
||||
slow = B.newLabel("bytes.version.slow"),
|
||||
done = B.newLabel("bytes.version.done");
|
||||
emitBytesLoopVersionGuard(emitter, version, fast, slow);
|
||||
const unitsFast =
|
||||
version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined;
|
||||
const sharedFast =
|
||||
version.sharedCopy && unitsFast === undefined
|
||||
? B.newLabel("bytes.version.fast.shared")
|
||||
: undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast, sharedFast);
|
||||
const units = guard?.units ?? null;
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
originalRanges = emitter.integerRanges;
|
||||
originalRanges = emitter.integerRanges,
|
||||
originalUnitReads = emitter.stringUnitReads,
|
||||
originalLengthReads = emitter.stringLengthReads,
|
||||
originalScopes = emitter.bytesAliasScopes;
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
// A guard that requires thread-owned storage fails for every view of
|
||||
// shared memory, which worker programs pass routinely: that checked
|
||||
// copy keeps its loops' speculative and int32 kind copies.
|
||||
if (version.ownedReceivers.length > 0) emitter.emitStmt(s);
|
||||
else emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
|
||||
// Both fast copies run under the guard's span test, so they share the
|
||||
// version's scopes, and each declares them on entry.
|
||||
const scoped =
|
||||
version.scopes && emitter.debug === null ? bytesAliasScopes(emitter, version.scopes) : null;
|
||||
if (scoped) emitter.bytesAliasScopes = new Map([...originalScopes, ...scoped.attachments]);
|
||||
if (version.counted?.guarded || version.paired?.loop.guarded)
|
||||
LOOPS.of(emitter).provenCountedGuards.add(s);
|
||||
B.startBlock(fast);
|
||||
declareAliasScopes(emitter, scoped?.scopes ?? []);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
// Both fast copies read the string's length the guard took.
|
||||
const length = guard?.length;
|
||||
if (length != null && version.unitLengths.size > 0)
|
||||
emitter.stringLengthReads = new Map([
|
||||
...originalLengthReads,
|
||||
...[...version.unitLengths].map((node) => [node, length] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
if (sharedFast !== undefined) {
|
||||
// The bounds hold and a view has shared storage: the accesses skip
|
||||
// their bounds checks and stay shared-safe, without alias scopes
|
||||
// (which worker programs give only thread-owned views).
|
||||
B.startBlock(sharedFast);
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.bytesAliasScopes = originalScopes;
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
if (unitsFast !== undefined && units !== null) {
|
||||
// The string reads take the receiver's UTF-16 units.
|
||||
B.startBlock(unitsFast);
|
||||
declareAliasScopes(emitter, scoped?.scopes ?? []);
|
||||
emitter.bytesBounds = new Set([
|
||||
...originalBounds,
|
||||
...[...version.bounds].filter((node) => !version.stringReads.has(node)),
|
||||
]);
|
||||
emitter.stringUnitReads = new Map([
|
||||
...originalUnitReads,
|
||||
...[...version.stringReads].map((node) => [node, units] as const),
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.stringUnitReads = originalUnitReads;
|
||||
emitter.stringLengthReads = originalLengthReads;
|
||||
emitter.bytesAliasScopes = originalScopes;
|
||||
LOOPS.of(emitter).provenCountedGuards.delete(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
|
||||
}
|
||||
@@ -2120,6 +2304,7 @@ export function emitLoopCopies(
|
||||
const plan =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planSpeculativeLoop(
|
||||
@@ -2136,6 +2321,7 @@ export function emitLoopCopies(
|
||||
);
|
||||
},
|
||||
(id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id),
|
||||
emitter.speculationContext(),
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
@@ -2147,6 +2333,7 @@ export function emitLoopCopies(
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
@@ -2193,6 +2380,20 @@ export function emitLoopCopies(
|
||||
B.line(`${both} = and i1 ${ok}, ${trying}`);
|
||||
ok = both;
|
||||
}
|
||||
// Hoisted leaves live in locals of their own, which the resume copy of
|
||||
// their write reads (see SpeculativeValue).
|
||||
for (const { leaves } of plan.values.values())
|
||||
for (const leaf of leaves) {
|
||||
emitter.currentLocals.set(leaf.localId, {
|
||||
id: leaf.localId,
|
||||
name: "speculated leaf",
|
||||
type: leaf.expr.type,
|
||||
mutable: true,
|
||||
});
|
||||
B.entryAllocas.push(
|
||||
`${emitter.binding(leaf.localId).slot} = alloca double ; speculated leaf`,
|
||||
);
|
||||
}
|
||||
const fast = B.newLabel("spec.fast"),
|
||||
slow = B.newLabel("spec.slow");
|
||||
B.condBr(ok, fast, slow);
|
||||
@@ -2218,6 +2419,7 @@ export function emitLoopCopies(
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header");
|
||||
B.startBlock(B.newLabel("spec.tail"));
|
||||
emitter.speculationResume = resume;
|
||||
emitter.speculationResumeValues = plan.values;
|
||||
emitter.speculationHeader = () => {
|
||||
B.br(header!);
|
||||
B.startBlock(B.newLabel("spec.unused"));
|
||||
@@ -2226,6 +2428,7 @@ export function emitLoopCopies(
|
||||
emitCopy(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationResumeValues = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
B.startBlock(fast);
|
||||
@@ -2274,6 +2477,7 @@ export function emitLoopCopies(
|
||||
deferred,
|
||||
bails,
|
||||
ran,
|
||||
values: plan.values,
|
||||
};
|
||||
try {
|
||||
const leave = B.newLabel("spec.leave");
|
||||
@@ -2421,6 +2625,102 @@ function emitInt32KindLoop(
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. */
|
||||
const WORKER_POLL_BLOCK = 64;
|
||||
|
||||
/** The 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. */
|
||||
interface StripMinedCounter {
|
||||
slot: string;
|
||||
type: string;
|
||||
signed: boolean;
|
||||
step: number;
|
||||
followers: { slot: string; step: number }[];
|
||||
limit: () => string;
|
||||
}
|
||||
|
||||
/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK
|
||||
* iterations, with the cancellation poll between blocks: the outer header
|
||||
* tests the counter against the loop's limit, each block runs the body up
|
||||
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
|
||||
* follows the block. It polls as often as the budget it replaces, and the
|
||||
* inner loop's only exit besides `break` and `return` is its bound. */
|
||||
function emitStripMinedCountedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: Extract<IrStmt, { kind: "for" }>,
|
||||
counter: StripMinedCounter,
|
||||
boundSlot: string,
|
||||
exit: string,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const { slot, type, signed, step } = counter;
|
||||
const ascending = step > 0;
|
||||
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
|
||||
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
|
||||
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
|
||||
const wrap = signed ? "nsw" : "nuw";
|
||||
const lc = B.newLabel("loop.c");
|
||||
const block = B.newLabel("loop.block");
|
||||
const bc = B.newLabel("loop.bc");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu = B.newLabel("loop.u");
|
||||
const poll = B.newLabel("loop.poll");
|
||||
emitter.declare(`declare ${type} @${saturating}(${type}, ${type})`);
|
||||
emitter.declare(`declare ${type} @${nearer}(${type}, ${type})`);
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
enterLoopHeader(emitter, lc);
|
||||
const start = B.tmp();
|
||||
const open = B.tmp();
|
||||
B.line(`${start} = load ${type}, ptr ${slot}`);
|
||||
const limit = counter.limit();
|
||||
B.line(`${open} = icmp ${compare} ${type} ${start}, ${limit}`);
|
||||
B.condBr(open, block, exit);
|
||||
B.startBlock(block);
|
||||
const stepped = B.tmp();
|
||||
const bound = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
|
||||
);
|
||||
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
|
||||
B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
|
||||
B.br(bc);
|
||||
B.startBlock(bc);
|
||||
const index = B.tmp();
|
||||
const end = B.tmp();
|
||||
const inBlock = B.tmp();
|
||||
B.line(`${index} = load ${type}, ptr ${slot}`);
|
||||
B.line(`${end} = load ${type}, ptr ${boundSlot}`);
|
||||
B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`);
|
||||
B.condBr(inBlock, lb, poll);
|
||||
B.startBlock(lb);
|
||||
emitter.jumpTargets.push({
|
||||
kind: "loop",
|
||||
brkLabel: exit,
|
||||
contLabel: lu,
|
||||
labels: s.labels,
|
||||
frameDepth: emitter.frames.length,
|
||||
scopeDepth: emitter.scopes.length,
|
||||
finallyDepth: emitter.finallyStack.length,
|
||||
});
|
||||
emitter.emitBlock(s.body);
|
||||
emitter.jumpTargets.pop();
|
||||
B.br(lu);
|
||||
B.startBlock(lu);
|
||||
for (const { slot: stepSlot, step: by } of [{ slot, step }, ...counter.followers]) {
|
||||
const old = B.tmp();
|
||||
const next = B.tmp();
|
||||
B.line(`${old} = load ${type}, ptr ${stepSlot}`);
|
||||
B.line(`${next} = add ${wrap} ${type} ${old}, ${by}`);
|
||||
B.line(`store ${type} ${next}, ptr ${stepSlot}`);
|
||||
}
|
||||
B.br(bc);
|
||||
B.startBlock(poll);
|
||||
emitter.emitWorkerPoll();
|
||||
B.br(lc);
|
||||
}
|
||||
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
const dispatch = emitter.speculationHeader;
|
||||
emitter.speculationHeader = null;
|
||||
@@ -2437,7 +2737,7 @@ function planProvenLoop(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
|
||||
): ProvenLoopPlan | null {
|
||||
if (
|
||||
loop.kind !== "for" ||
|
||||
loop.kind === "doWhile" ||
|
||||
!emitter.speculativeLoopsEnabled ||
|
||||
emitter.speculation !== null ||
|
||||
emitter.speculationResume !== null ||
|
||||
@@ -2531,9 +2831,22 @@ export function emitSpeculativeWrite(
|
||||
// at once: a bailing path then owns nothing, and reads have no effect
|
||||
// whose reordering before the arithmetic could be observed.
|
||||
const reads = new Map<IrExpr, LlValue>();
|
||||
if (target.value !== null) {
|
||||
const rewritten = emitter.speculation!.values?.get(s);
|
||||
const value = rewritten?.value ?? target.value;
|
||||
if (rewritten !== undefined) {
|
||||
// Hoisted leaves run first, in the write's order, and keep their
|
||||
// doubles for the resume copy.
|
||||
emitter.frames.push([]);
|
||||
for (const read of speculativeReads(target.value)) reads.set(read, emitter.emitExpr(read));
|
||||
for (const leaf of rewritten.leaves) {
|
||||
const evaluated = emitter.emitExpr(leaf.expr);
|
||||
B.line(`store double ${evaluated.name}, ptr ${emitter.binding(leaf.localId).slot}`);
|
||||
reads.set(leaf.ref, evaluated);
|
||||
}
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
}
|
||||
if (value !== null) {
|
||||
emitter.frames.push([]);
|
||||
for (const read of speculativeReads(value)) reads.set(read, emitter.emitExpr(read));
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
}
|
||||
emitter.frames.push([]);
|
||||
@@ -2548,7 +2861,7 @@ export function emitSpeculativeWrite(
|
||||
B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`);
|
||||
B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`);
|
||||
bail(outside);
|
||||
} else result = emitSpeculativeInteger(emitter, target.value, bail, reads, resume === undefined);
|
||||
} else result = emitSpeculativeInteger(emitter, value!, bail, reads, resume === undefined);
|
||||
// Speculable values read only numbers past this point.
|
||||
if (emitter.frames.pop()!.length > 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -235,6 +235,26 @@ export function emitBytesGet(
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): LlValue {
|
||||
const outer = host.bytesAccessScope;
|
||||
host.bytesAccessScope = scope;
|
||||
try {
|
||||
return emitScopedBytesGet(host, elem, receiver, index, expr, inBounds, invalidNaN, owned);
|
||||
} finally {
|
||||
host.bytesAccessScope = outer;
|
||||
}
|
||||
}
|
||||
|
||||
function emitScopedBytesGet(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
index: LlValue,
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
invalidNaN: boolean,
|
||||
owned: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
// Any invalid index traps (the array runtime's discipline).
|
||||
@@ -547,6 +567,27 @@ export function emitBytesSet(
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): void {
|
||||
const outer = host.bytesAccessScope;
|
||||
host.bytesAccessScope = scope;
|
||||
try {
|
||||
emitScopedBytesSet(host, elem, receiver, index, value, indexExpr, expr, inBounds, owned);
|
||||
} finally {
|
||||
host.bytesAccessScope = outer;
|
||||
}
|
||||
}
|
||||
|
||||
function emitScopedBytesSet(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
index: LlValue,
|
||||
value: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
owned: boolean,
|
||||
): void {
|
||||
const B = host.B;
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
@@ -768,6 +809,7 @@ export function emitBytesIntrinsic(
|
||||
host.bytesBounds.has(e),
|
||||
e.invalidNaN === true,
|
||||
host.bytesOwned.has(e),
|
||||
host.bytesAliasScopes.get(e),
|
||||
);
|
||||
case "slice":
|
||||
return call(
|
||||
|
||||
@@ -244,38 +244,46 @@ export function emitCallExpr(
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`;
|
||||
// A call of an unwinding-ABI callee is an invoke (emitDirectCall):
|
||||
// its landing pad unwinds, so no pending check follows it.
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (abi?.union) {
|
||||
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
if (stackResult) return host.stackScalarUnion(t, e.type);
|
||||
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
|
||||
}
|
||||
if (abi?.result) {
|
||||
B.line(`${t} = call i32 ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee);
|
||||
const out = host.int32Result(t, e.type);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
let out: LlValue;
|
||||
if (borrowed && host.borrowedReturns.has(e.callee)) {
|
||||
// A borrowed-return body hands back +0 (a null dummy when it threw):
|
||||
// test the exception first, then own the result before the
|
||||
// argument snapshots that keep it reachable are released.
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
out = host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
} else {
|
||||
out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
}
|
||||
if (borrowed) {
|
||||
host.toFrame(out);
|
||||
@@ -1366,10 +1374,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
|
||||
)
|
||||
.join(", ");
|
||||
const t = host.B.tmp();
|
||||
host.B.line(
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`,
|
||||
e.callee,
|
||||
);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
emitStringMapGetRef,
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapHas,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
emitUnitCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -322,6 +324,15 @@ export function charAtUnitRead(
|
||||
return read;
|
||||
}
|
||||
|
||||
/** The UTF-16 units an enclosing loop version proved the index of `e`
|
||||
* below the count of (string-unit-versions.ts). */
|
||||
function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null {
|
||||
const units = host.stringUnitReads.get(e);
|
||||
if (units !== undefined) return units;
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null;
|
||||
}
|
||||
|
||||
/** An enclosing loop version proved the string of `e` ASCII and its index
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
@@ -364,9 +375,14 @@ export function emitStrIntrinsic(
|
||||
const method = e.method;
|
||||
switch (method) {
|
||||
case "length": {
|
||||
const t = B.tmp();
|
||||
const length = host.stringLengthReads.get(e);
|
||||
if (length !== undefined) {
|
||||
B.line(`${t} = sitofp i64 ${length} to double`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline.
|
||||
host.declare(STRING_UTF16_LEN_DECL);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -376,6 +392,8 @@ export function emitStrIntrinsic(
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
const wide = widenInteger(host, index);
|
||||
const units = provenUnitsRead(host, e);
|
||||
if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type };
|
||||
if (provenStringRead(host, e))
|
||||
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
@@ -1127,6 +1145,65 @@ export function emitArrIntrinsic(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `e` is a Map.get that emitBorrowedMapGet can emit without a
|
||||
* retain: a typed map with number or inline string keys, a reference value,
|
||||
* and a nullable result (the value's pointer, or the absent arm's). */
|
||||
export function borrowableMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== "get" || e.args.length !== 1) return false;
|
||||
if (!borrowsMapReadInputs(e) || e.receiver.type.kind !== "map") return false;
|
||||
const { key, value } = e.receiver.type;
|
||||
if (!isRefCounted(value) || value.kind === "union" || key.kind === "union") return false;
|
||||
if (e.type.kind !== "union" || !host.nullableUnions.has(e.type.unionId)) return false;
|
||||
const arg = e.args[0]!;
|
||||
const access = mapKeyAccess(key);
|
||||
return (
|
||||
(access === "f64" && arg.type.kind === "f64") ||
|
||||
(access === "str" && arg.type.kind === "string")
|
||||
);
|
||||
}
|
||||
|
||||
/** A borrowableMapGet read as the entry's own reference: no retain, and
|
||||
* no owner. The caller proves that nothing can change the map while the
|
||||
* value is used (findMapReadBorrows). Null, before emitting anything, for
|
||||
* a key held as a split span, which takes the runtime's owned lookup. */
|
||||
export function emitBorrowedMapGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "mapIntrinsic" },
|
||||
): LlValue | null {
|
||||
const arg = e.args[0]!;
|
||||
if (arg.kind === "varRef" && host.splitSpans.get(arg.localId) !== undefined) return null;
|
||||
const B = host.B;
|
||||
const mapType = e.receiver.type as IrType & { kind: "map" };
|
||||
const r = host.emitStableReceiver(e.receiver, e.args);
|
||||
const k = emitMapLookupKey(host, e.args[0]!, true);
|
||||
const numberKey = inlineNumberKey(k);
|
||||
const stringKey = entryStringKey(k);
|
||||
const value = mapType.value;
|
||||
let raw: string;
|
||||
if (numberKey !== null) raw = emitNumberMapGetRef(host, r.name, numberKey, value, true);
|
||||
else if (stringKey !== null)
|
||||
raw = emitStringMapGetRef(
|
||||
host,
|
||||
r.name,
|
||||
stringKey,
|
||||
value,
|
||||
compactMapValues(host, mapType.key, value),
|
||||
true,
|
||||
);
|
||||
else throw new InternalCompilerError("llvm emitter bug: borrowed Map.get without an inline key");
|
||||
const resultType = e.type as IrType & { kind: "union" };
|
||||
const absent = host.unitInstanceRef(
|
||||
resultType.unionId,
|
||||
undefinedArmTag(resultType, host.unionsById),
|
||||
);
|
||||
if (absent === "null") return { name: raw, type: e.type };
|
||||
const isnull = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
export function wrapNullable(
|
||||
host: LlvmEmitterContext,
|
||||
raw: string,
|
||||
@@ -1202,7 +1279,9 @@ function collectionOps(host: LlvmEmitterContext, key: IrType, value: IrType): st
|
||||
function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): string {
|
||||
const B = host.B;
|
||||
const arms = key.kind === "union" ? host.unionsById.get(key.unionId)?.arms : undefined;
|
||||
const kind = mapKeyKindNum(key, arms);
|
||||
// SCR_MAP_KEY_REF (2): shape-union keys are record pointers.
|
||||
const kind =
|
||||
key.kind === "union" && host.nullableUnions.shapes(key.unionId) ? 2 : mapKeyKindNum(key, arms);
|
||||
const m = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`);
|
||||
// SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues).
|
||||
@@ -1461,6 +1540,7 @@ export function emitMapLikeIntrinsic(
|
||||
: emitStringMapGetScalar(host, r.name, stringKey!);
|
||||
found = scalar.found;
|
||||
if (stringKey !== null) rememberMapEntry(host, e, r.name, stringKey, scalar.entry);
|
||||
else rememberMapEntry(host, e, r.name, numberKey!, scalar.entry, "double");
|
||||
readHit = () => {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
@@ -1528,11 +1608,18 @@ export function emitMapLikeIntrinsic(
|
||||
// Key borrowed (the runtime retains stored string keys); the
|
||||
// value MOVES in (replacement releases the old value inside).
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!);
|
||||
const v = host.emitExpr(e.args[1]!);
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.toHeap(v);
|
||||
const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name);
|
||||
if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
if (
|
||||
(kAcc === "str" || kAcc === "f64") &&
|
||||
vAcc !== "ref" &&
|
||||
emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
) {
|
||||
// Written through the read's entry, or inserted.
|
||||
} else if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) {
|
||||
const bits = B.tmp();
|
||||
B.line(
|
||||
vAcc === "f64"
|
||||
@@ -1540,12 +1627,7 @@ export function emitMapLikeIntrinsic(
|
||||
: `${bits} = zext i1 ${v.name} to i64`,
|
||||
);
|
||||
emitNumberMapSetScalar(host, r.name, k.name, bits, ordinary);
|
||||
} else if (
|
||||
kAcc !== "str" ||
|
||||
vAcc === "ref" ||
|
||||
!emitFusedMapSet(host, e, r.name, k.name, { name: v.name, access: vAcc }, ordinary)
|
||||
)
|
||||
ordinary();
|
||||
} else ordinary();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "add": {
|
||||
@@ -1553,6 +1635,7 @@ export function emitMapLikeIntrinsic(
|
||||
// Element borrowed (the runtime retains stored strings); the unit
|
||||
// value is 0. Re-adding overwrites in place, preserving insertion.
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!);
|
||||
if (kAcc === "f64" && host.readNumberMapWrites.has(e))
|
||||
emitNumberMapSetScalar(host, r.name, k.name, null, () =>
|
||||
host.mapSet(r.name, kAcc, "f64", k.name, f64Lit(0)),
|
||||
@@ -1562,7 +1645,10 @@ export function emitMapLikeIntrinsic(
|
||||
}
|
||||
case "has": {
|
||||
const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
if (kAcc === "f64") {
|
||||
noteNumberMapKey(host, k, e.args[0]!);
|
||||
return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
}
|
||||
if (kAcc === "str")
|
||||
return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type };
|
||||
host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`);
|
||||
|
||||
@@ -30,6 +30,9 @@ export interface WideInteger {
|
||||
name: string;
|
||||
below: number;
|
||||
bound: number;
|
||||
/** The double is the integer rounded once: the result of one operation on
|
||||
* operands that always equal their doubles. */
|
||||
single?: true;
|
||||
}
|
||||
|
||||
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DYN_KIND } from "./dyn.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
|
||||
import { emitUnionTag } from "./union-repr.js";
|
||||
|
||||
export function emitControlExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -52,6 +54,8 @@ export function emitControlExpr(
|
||||
case "toBool":
|
||||
return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type };
|
||||
case "logical": {
|
||||
const tagSet = e.op === "||" ? unionTagSet(e) : null;
|
||||
if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type };
|
||||
// JS value semantics: the result is the deciding operand itself.
|
||||
// Left evaluates once, ownership moves into the result slot; when
|
||||
// the branch takes the right operand the stale left releases first
|
||||
@@ -614,3 +618,85 @@ function literalChoice(e: IrExpr): boolean {
|
||||
if (e.kind === "strLit") return true;
|
||||
return e.kind === "ternary" && literalChoice(e.then) && literalChoice(e.else_);
|
||||
}
|
||||
|
||||
type TagSet = { unionId: string; value: IrExpr; tags: Set<number> };
|
||||
|
||||
/** An `||` chain of two or more tag tests of one local (`isTag(u, 1) ||
|
||||
* isTag(u, 4) || ...`, the shape of a union conversion's identity test). */
|
||||
function unionTagSet(e: IrExpr & { kind: "logical" }): TagSet | null {
|
||||
const leaves: IrExpr[] = [];
|
||||
const collect = (x: IrExpr): void => {
|
||||
if (x.kind === "logical" && x.op === "||") {
|
||||
collect(x.left);
|
||||
collect(x.right);
|
||||
} else leaves.push(x);
|
||||
};
|
||||
collect(e);
|
||||
const first = leaves[0]!;
|
||||
if (first.kind !== "unionIsTag" || first.value.kind !== "varRef") return null;
|
||||
const localId = first.value.localId;
|
||||
const tags = new Set<number>();
|
||||
for (const leaf of leaves) {
|
||||
if (
|
||||
leaf.kind !== "unionIsTag" ||
|
||||
leaf.negated ||
|
||||
leaf.unionId !== first.unionId ||
|
||||
leaf.value.kind !== "varRef" ||
|
||||
leaf.value.localId !== localId
|
||||
)
|
||||
return null;
|
||||
tags.add(leaf.tag);
|
||||
}
|
||||
return { unionId: first.unionId, value: first.value, tags };
|
||||
}
|
||||
|
||||
/** i1: the union value holds one of the tags. One read of the tag and one
|
||||
* switch over the set; a shape union switches on the record's shape word
|
||||
* after its unit encodings. */
|
||||
function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string {
|
||||
const B = host.B;
|
||||
const u = host.emitUnionProjection(set.value).name;
|
||||
const nullable = host.nullableUnions.get(set.unionId);
|
||||
const yes = B.newLabel("tags.y");
|
||||
const no = B.newLabel("tags.n");
|
||||
const join = B.newLabel("tags.j");
|
||||
const target = (tag: number): string => (set.tags.has(tag) ? yes : no);
|
||||
if (nullable?.repr === "shapes") {
|
||||
if (nullable.unitTag >= 0) {
|
||||
const isNull = B.tmp();
|
||||
const next = B.newLabel("tags.r");
|
||||
B.line(`${isNull} = icmp eq ptr ${u}, null`);
|
||||
B.condBr(isNull, target(nullable.unitTag), next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
if (nullable.nullTag >= 0) {
|
||||
const isNullArm = B.tmp();
|
||||
const next = B.newLabel("tags.r");
|
||||
B.line(`${isNullArm} = icmp eq ptr ${u}, ${NULLABLE_NULL}`);
|
||||
B.condBr(isNullArm, target(nullable.nullTag), next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
const wp = B.tmp();
|
||||
const w = B.tmp();
|
||||
B.line(
|
||||
`${wp} = getelementptr inbounds ${shapeWordHeader(host.sizeType)}, ptr ${u}, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
);
|
||||
B.line(`${w} = load i32, ptr ${wp}`);
|
||||
const cases = nullable.arms
|
||||
.filter((arm) => set.tags.has(arm.tag))
|
||||
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %${yes}`);
|
||||
B.terminate(`switch i32 ${w}, label %${no} [ ${cases.join(" ")} ]`);
|
||||
} else {
|
||||
const tag = nullable ? emitUnionTag(B, nullable, u) : host.unionTag(u, set.unionId);
|
||||
const cases = [...set.tags].map((t) => `i32 ${t}, label %${yes}`);
|
||||
B.terminate(`switch i32 ${tag}, label %${no} [ ${cases.join(" ")} ]`);
|
||||
}
|
||||
B.startBlock(yes);
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`);
|
||||
return t;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user