Merge ni/i4-deferred and apply parity and security fixes

This commit is contained in:
Chris Tate
2026-10-25 22:26:54 -05:00
1293 changed files with 75480 additions and 11715 deletions
+38 -2
View File
@@ -354,6 +354,37 @@ jobs:
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
- run: pnpm test tests/harness/effect.test.ts tests/harness/effect4.test.ts
# The sanitized three.js nodes build alone: beside the san corpus's other
# workers it took 860 to more than 1200 seconds of its shard
# (SCRIPTC_THREE_SANITIZED_NODES_JOB in scripts/ci-test-plan.mjs). It keeps
# the corpus's LeakSanitizer settings.
three_nodes_san:
needs: [validation_scope, lanes, native_linux]
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
name: test (Linux, san, three.js nodes)
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
SCRIPTC_SAN: "1"
ASAN_OPTIONS: detect_leaks=1
LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/harness/lsan.supp:print_suppressions=0
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.1.3
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@v4
with:
name: native-linux-x64-gnu
path: ${{ runner.temp }}
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
- run: pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'"
# Build each instrumented seed once. Downstream contracts retain the same
# compiler and source artifacts without repeating seed construction.
bootstrap_seed:
@@ -547,6 +578,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
@@ -641,8 +674,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:
@@ -955,6 +989,7 @@ jobs:
tests_macos,
native_linux,
effects,
three_nodes_san,
bootstrap_seed,
bootstrap,
linux_host_clang,
@@ -987,6 +1022,7 @@ jobs:
lane tests_macos "${{ needs.tests_macos.result }}" "${{ needs.lanes.outputs.macos }}"
lane native_linux "${{ needs.native_linux.result }}" "${{ needs.lanes.outputs.linux }}"
lane effects "${{ needs.effects.result }}" "${{ needs.lanes.outputs.effects }}"
lane three_nodes_san "${{ needs.three_nodes_san.result }}" "${{ needs.lanes.outputs.linux }}"
lane bootstrap_seed "${{ needs.bootstrap_seed.result }}" "${{ needs.lanes.outputs.bootstrap }}"
lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}"
lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}"
+41
View File
@@ -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"
+88 -2
View File
@@ -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:
@@ -559,6 +630,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 }}"
+2
View File
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
Name a new corpus program after what it tests, without a number: lowercase words joined by hyphens, such as `tests/corpus/intersection-restated-members.ts`, or a directory such as `tests/corpus/<name>/main.ts` for a multi-file program. Parallel changes kept taking the same next number; with names, two changes collide only when they add the same name, which git reports as a conflict. Leave existing numbered programs as they are: never rename or renumber them, and name a new program even when it extends a numbered family.
Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/<program>.json`, not in `tests/harness/corpus-costs.json`; see the README there.
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
+36 -2
View File
@@ -6,6 +6,42 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.4.5
### Highlights
- **Fixes:**
- Release builds read a typed array's length without a header load that could read a freed view.
- `net.Socket` `write()` returns Node's backpressure boolean and fires `'drain'`.
- Super calls in JavaScript method values call the declared base method.
- Calling a non-function through an asserted callee throws Node's TypeError.
- **Faster programs:**
- Exceptions propagate by unwinding from directly called functions on Linux, instead of status checks after every call.
- Typed-array loops prove their bounds and keep their views disjoint in more cases, including views over shared memory in worker programs; integer loops stay in integers across nested counted loops, pure helper calls, powers of two and byte reads.
- Lone array callbacks run as loops with their bodies inlined, and row sums run two rows per pass.
- Number-key maps look keys up on their integer view and write updates through the entry the read found; small number-key maps use a direct index.
- Strings: base64 decodes eight characters at a time; Buffers fill, swap byte order and decode `utf16le` a word at a time; readline splits input in one scan; string scans over non-ASCII strings use their UTF-16 units.
- Untyped code: property caches also cache absent properties, object literal members go through the store caches, calls on plain arrays skip the method ladder, and dynamic values dispose without key-cache walks.
- JSON parses into discriminated unions, and dynamic values convert into nested unions, in one pass.
- BigInt division settles quotients from the top limbs, and limb shifts are vectorized.
- **Faster builds:**
- Function bodies are emitted on up to eight workers, including in dev builds, and repeated work in LLVM module emission is cut.
- Linux executables link with lld when the driver finds it, and unchanged executables are reused without selecting the runtime pack.
- Dev builds reuse their frontend output after edits that keep token positions, and unchanged frontend inputs are checked by file identity before hashing.
- Lowering fetches only the syntax trees it reads, and the large compiler tables are built on first use.
- Release builds of the native compiler use a profile collected on the release commit.
- Type checking can use a tsc-ts server shared by later builds (opt-in).
- **Node.js parity:**
- Crypto: RSA key generation lengths, RSA-PSS keys, encrypted private keys, `X509Certificate` fields and methods, and TLS certificates and `tls.Server` contexts.
- zlib: Brotli compression and decompression.
- Dates are mutable objects with Node's setters and conversions; URL hosts and paths parse as the URL Standard does, including non-ASCII hosts with UTS #46.
- `Math`, `JSON` and `Reflect` are real objects in JavaScript; objects can inherit from JavaScript class prototypes; typed arrays held as untyped values support named properties and `Symbol.species`.
- Error messages: `JSON.parse` syntax errors use V8's messages and positions; `Object.getPrototypeOf` of an Error answers `Error.prototype`.
- `util.inspect` of dynamic classes, functions and `%o` arguments, and `Symbol.toStringTag` fields and getters, match Node.
- **npm packages:** more npm and CommonJS dependencies compile statically, including namespace-style packages with guarded declarations and `var` exports, and untyped Maps, Sets and arrays are typed from their uses.
<!-- release:end -->
## 0.4.4
### Highlights
@@ -29,8 +65,6 @@ All notable changes to scriptc will be documented in this file.
- Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built.
- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary.
<!-- release:end -->
## 0.4.3
### Highlights
+1 -1
View File
@@ -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
+6
View File
@@ -10,6 +10,7 @@ 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 |
@@ -19,6 +20,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `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 |
@@ -28,12 +30,16 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset |
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `string-iteration` | cpu | code point iteration over text that mixes ASCII, Latin-1 letters, CJK and emoji: a for-of word tokenizer, spread reversal, and `Array.from` over words |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
@@ -0,0 +1,6 @@
{
"entry": "workloads/date-math.ts",
"args": [
"40"
]
}
+52
View File
@@ -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,7 @@
{
"entry": "workloads/line-reader.ts",
"input": "line-reader",
"args": [
"{input}"
]
}
@@ -0,0 +1,34 @@
// Line-by-line processing of a log file with readline over a file stream:
// 'line' events for about 67 MB of access-log lines, with light work per
// line (a status count, a path tally, byte totals).
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const path = process.argv[2]!;
const statuses = new Map<string, number>();
const paths = new Map<string, number>();
let lines = 0;
let chars = 0;
let bytes = 0;
let longest = 0;
const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
input.on("line", (line: string) => {
lines++;
chars += line.length;
if (line.length > longest) longest = line.length;
const quote = line.indexOf('"');
const space = line.indexOf(" ", quote);
const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1));
paths.set(path, (paths.get(path) ?? 0) + 1);
const close = line.indexOf('"', space);
const status = line.slice(close + 2, close + 5);
statuses.set(status, (statuses.get(status) ?? 0) + 1);
bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6)));
});
input.on("close", () => {
console.log(`${lines} lines, ${chars} characters, longest ${longest}`);
console.log(`bytes ${bytes}`);
for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`);
for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`);
});
@@ -0,0 +1,6 @@
{
"entry": "workloads/regex-replacers.ts",
"args": [
"1"
]
}
@@ -0,0 +1,60 @@
// Regex replace and replaceAll with inline callbacks, written as they
// usually are, without parameter annotations: template placeholders looked
// up in a dictionary, snake_case to camelCase, date reordering, optional
// version parts, capitalization and a string search with its offset.
let seed = 7;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function pick<T>(items: readonly T[]): T {
return items[Math.floor(random() * items.length)]!;
}
const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"];
const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"];
const vars: Record<string, string> = {
name: "world",
city: "Lisbon",
day: "Tuesday",
plan: "pro",
count: "12",
};
const keys = [...Object.keys(vars), "missing"];
const scale = Number(process.argv[2] ?? "1");
const count = Math.floor(20000 * scale);
const templates: string[] = [];
const identifiers: string[] = [];
const dates: string[] = [];
const versions: string[] = [];
const sentences: string[] = [];
for (let i = 0; i < count; i++) {
templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`);
identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`);
const year = 1990 + Math.floor(random() * 40);
const month = String(1 + Math.floor(random() * 12)).padStart(2, "0");
const day = String(1 + Math.floor(random() * 28)).padStart(2, "0");
dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`);
versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`);
sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`);
}
let length = 0;
let checksum = 0;
for (let round = 0; round < 4; round++) {
for (let i = 0; i < count; i++) {
const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`);
const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`);
const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`);
const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase());
const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m));
length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length;
checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0;
}
}
console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`));
console.log("length", length, "checksum", checksum);
@@ -0,0 +1,6 @@
{
"entry": "workloads/string-iteration.ts",
"args": [
"1"
]
}
@@ -0,0 +1,59 @@
// Code point iteration over mixed text: a word tokenizer over for-of, a
// character histogram, reversal through spread, and Array.from over
// words. The text mixes ASCII, Latin-1 letters, CJK and emoji.
let seed = 11;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const pieces = [
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
"café", "naïve", "über", "señor", "straße", "déjà", "vu",
"日本", "語", "テキスト", "中文", "한국어",
"😀", "🚀", "👍", "🎉",
];
const punctuation = [" ", " ", " ", ", ", ". ", "! ", "\n"];
const scale = Number(process.argv[2] ?? "1");
const lines: string[] = [];
for (let i = 0; i < Math.floor(4000 * scale); i++) {
let line = "";
const words = 6 + Math.floor(random() * 10);
for (let w = 0; w < words; w++) {
line += pieces[Math.floor(random() * pieces.length)]!;
line += punctuation[Math.floor(random() * punctuation.length)]!;
}
lines.push(line);
}
function isWordChar(c: string): boolean {
const code = c.codePointAt(0)!;
return (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || code >= 0xc0;
}
let tokens = 0;
let tokenUnits = 0;
let reversedLength = 0;
let fromLength = 0;
const histogram = new Map<string, number>();
for (let round = 0; round < 30; round++) {
for (const line of lines) {
let word = "";
for (const c of line) {
if (isWordChar(c)) {
word += c;
continue;
}
if (word !== "") {
tokens++;
tokenUnits += word.length;
if (round === 0) histogram.set(word, (histogram.get(word) ?? 0) + 1);
fromLength += Array.from(word).length;
word = "";
}
}
reversedLength += [...line].reverse().join("").length;
}
}
const top = [...histogram].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 5);
console.log(top.map(([word, count]) => `${word}:${count}`).join(" "));
console.log("tokens", tokens, "units", tokenUnits, "reversed", reversedLength, "from", fromLength);
@@ -0,0 +1,6 @@
{
"entry": "workloads/throw-chains.ts",
"args": [
"1"
]
}
@@ -0,0 +1,196 @@
// Exceptions that cross many frames: throws from helpers below a recursion,
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
// callback), with each frame holding an array across its call, and a
// backtracking parser that uses exceptions for control flow.
class ParseError extends Error {
readonly at: number;
constructor(at: number, message: string) {
super(message);
this.at = at;
}
}
/** A thrown value that is not an Error (no stack to capture). */
class Stop {
readonly code: number;
constructor(code: number) {
this.code = code;
}
}
const held: number[][] = [];
function fail(n: number, plain: boolean): number {
if (plain) throw new Stop(n);
throw new RangeError("chain " + n);
}
function check(n: number, plain: boolean): number {
return (n & 3) === 0 ? fail(n, plain) : n;
}
function validate(n: number, plain: boolean): number {
return check(n, plain) + (n & 1);
}
function bottom(n: number, plain: boolean): number {
const box = [n, -1];
const r = validate(n, plain);
held[0] = box;
return r + box.length;
}
function walk(n: number, depth: number, plain: boolean): number {
const box = [n, depth];
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
held[depth] = box;
return r + box.length;
}
function chains(rounds: number): string {
const out: string[] = [];
for (const depth of [2, 5, 20])
for (const plain of [false, true]) {
let caught = 0;
let sum = 0;
for (let i = 0; i < rounds; i++) {
try {
sum += walk(i, depth - 2, plain);
} catch (error) {
caught++;
sum += error instanceof Stop ? error.code & 7 : 1;
}
}
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
}
return out.join("\n");
}
// The same chains through a callback: each level visits the next with a
// closure, as tree visitors do.
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
const box = [n, depth];
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
held[depth] = box;
return r + box.length;
}
function visits(rounds: number): string {
const out: string[] = [];
const each = (n: number, depth: number): number => visit(n, depth, each);
for (const depth of [2, 5, 20]) {
let caught = 0;
let sum = 0;
for (let i = 0; i < rounds; i++) {
try {
sum += visit(i, depth - 2, each);
} catch (error) {
caught++;
sum += error instanceof RangeError ? 1 : 0;
}
}
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
}
return out.join("\n");
}
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
// A call and a name start alike: the parser tries the call first and
// backtracks when it fails.
let text = "";
let pos = 0;
function peek(): string {
return pos < text.length ? text[pos]! : "";
}
function expect(c: string): void {
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
pos++;
}
function name(): string {
const start = pos;
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
if (pos === start) throw new ParseError(pos, "expected a name");
return text.slice(start, pos);
}
function call(): number {
const callee = name();
expect("(");
let value = expr() + callee.length;
while (peek() === ",") {
pos++;
value += expr();
}
expect(")");
return value;
}
function atom(): number {
const c = peek();
if (c >= "0" && c <= "9") {
let value = 0;
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
return value;
}
if (c === "(") {
pos++;
const value = expr();
expect(")");
return value;
}
const start = pos;
try {
return call();
} catch (error) {
if (!(error instanceof ParseError)) throw error;
pos = start;
return name().length;
}
}
function term(): number {
let value = atom();
while (peek() === "*") {
pos++;
value = (value * atom()) % 1000003;
}
return value;
}
function expr(): number {
let value = term();
while (peek() === "+" || peek() === "-") {
const op = text[pos++];
value = op === "+" ? value + term() : value - term();
}
return value;
}
let seed = 4242;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function source(depth: number): string {
const r = random();
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
if (r < 0.6) return `(${source(depth + 1)})`;
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
}
function parse(count: number): string {
let total = 0;
let failed = 0;
for (let i = 0; i < count; i++) {
// Every tenth input lacks its last character, which fails deep inside it.
const full = source(0);
text = i % 10 === 0 ? full.slice(0, -1) : full;
pos = 0;
try {
total = (total + expr()) % 1000003;
if (pos !== text.length) throw new ParseError(pos, "trailing input");
} catch (error) {
if (!(error instanceof ParseError)) throw error;
failed++;
}
}
return `parsed ${count}: total ${total} failed ${failed}`;
}
const scale = Number(process.argv[2] ?? "1");
console.log(chains(Math.floor(60000 * scale)));
console.log(visits(Math.floor(60000 * scale)));
console.log(parse(Math.floor(40000 * scale)));
@@ -0,0 +1,6 @@
{
"entry": "workloads/union-node-sets.ts",
"args": [
"1"
]
}
@@ -0,0 +1,141 @@
// Syntax-tree passes over a discriminated union of object literals, the way
// compilers walk their IR: a DAG of shared subexpressions, a `Set` of
// visited nodes, a `Map` from node to its value, and conversions between
// the whole union and the sub-unions passes narrow it to.
type Num = { kind: "num"; value: number };
type Var = { kind: "var"; slot: number };
type Neg = { kind: "neg"; operand: Node };
type Add = { kind: "add"; left: Node; right: Node };
type Mul = { kind: "mul"; left: Node; right: Node };
type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node };
type Leaf = Num | Var;
type Node = Leaf | Neg | Add | Mul | Pick;
function children(node: Node, visit: (child: Node) => void): void {
switch (node.kind) {
case "num":
case "var":
return;
case "neg":
visit(node.operand);
return;
case "add":
case "mul":
visit(node.left);
visit(node.right);
return;
case "pick":
visit(node.cond);
visit(node.then);
visit(node.else_);
return;
}
}
function leafValue(leaf: Leaf, env: number[]): number {
return leaf.kind === "num" ? leaf.value : env[leaf.slot]!;
}
function evaluate(node: Node, env: number[], memo: Map<Node, number>): number {
const cached = memo.get(node);
if (cached !== undefined) return cached;
let result: number;
switch (node.kind) {
case "num":
case "var":
result = leafValue(node, env);
break;
case "neg":
result = -evaluate(node.operand, env, memo);
break;
case "add":
result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003;
break;
case "mul":
result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003;
break;
case "pick":
result =
evaluate(node.cond, env, memo) % 2 === 0
? evaluate(node.then, env, memo)
: evaluate(node.else_, env, memo);
break;
}
memo.set(node, result);
return result;
}
function reachable(root: Node): number {
const seen = new Set<Node>();
const stack: Node[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (seen.has(node)) continue;
seen.add(node);
children(node, (child) => {
if (!seen.has(child)) stack.push(child);
});
}
return seen.size;
}
function leaves(root: Node): Leaf[] {
const out: Leaf[] = [];
const seen = new Set<Node>();
const walk = (node: Node): void => {
if (seen.has(node)) return;
seen.add(node);
if (node.kind === "num" || node.kind === "var") out.push(node);
else children(node, walk);
};
walk(root);
return out;
}
let seed = 2463534242;
function next(): number {
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return seed >>> 0;
}
function build(size: number): Node {
const pool: Node[] = [];
for (let i = 0; i < 64; i++)
pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 });
// Operands come from the most recent nodes, so most of the DAG is
// reachable from the root and many nodes are shared.
const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!;
for (let i = 0; i < size; i++) {
const a = recent();
const b = recent();
const r = next() % 10;
let node: Node;
if (r < 1) node = { kind: "neg", operand: a };
else if (r < 5) node = { kind: "add", left: a, right: b };
else if (r < 8) node = { kind: "mul", left: a, right: b };
else node = { kind: "pick", cond: a, then: b, else_: recent() };
pool.push(node);
}
return pool[pool.length - 1]!;
}
const scale = Number(process.argv[2] ?? "1");
const rounds = Math.max(1, Math.round(40 * scale));
let checksum = 0;
let nodes = 0;
let leafCount = 0;
for (let round = 0; round < rounds; round++) {
const root = build(20_000);
nodes += reachable(root);
const found = leaves(root);
leafCount += found.length;
for (let pass = 0; pass < 4; pass++) {
const env: number[] = [];
for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97);
checksum = (checksum + evaluate(root, env, new Map())) % 1000003;
for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003;
}
}
console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum);
+3
View File
@@ -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
+1 -1
View File
@@ -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
+52
View File
@@ -141,6 +141,58 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports.
## Fix a package's types
scriptc trusts declared types: a package's `.d.ts` files, its `@types/` package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a `TypeError` instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared `string` or `number` that is sometimes `undefined`.
Fix such a declaration in your project with a type patch in `package.json`:
```json:package.json
{
"scriptc": {
"typePatches": {
"commander@^15.0.0": {
"declarations": {
"HelpContext.helpWidth": "| undefined"
}
}
}
}
}
```
Each key names a package and a semver range of the installed versions the patch describes. `declarations` maps a declaration to a type:
<dl>
<dt>`"| undefined"`</dt>
<dd>A type that starts with `|` widens the declared type, here to `number | undefined`.</dd>
<dt>`"string | null"`</dt>
<dd>Any other type replaces the declared type.</dd>
</dl>
A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue.
For larger fixes, replace a whole declaration file, or give a package without declarations its own:
```json:package.json
{
"scriptc": {
"typePatches": {
"some-client@^3.2.0": {
"files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
},
"untyped-helper@1.x": {
"types": "./type-patches/untyped-helper.d.ts"
}
}
}
}
```
`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types/<name>/<path>`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.
Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.
## Provenance sources (experimental)
`--provenance-sources` compiles a package from the source at the commit its npm provenance attestation names, instead of from its published JavaScript. This lets packages written in TypeScript compile from their original TypeScript. Packages without a usable attestation compile from their installed files as usual.
+8
View File
@@ -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.
@@ -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.
@@ -14,7 +14,7 @@ A class can extend `require("events")` directly.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`.
`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions.
Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise.
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`.
@@ -1,6 +1,13 @@
`console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr.
They accept `unknown`, printing strings verbatim and other values through native `util.inspect`.
Direct calls support global `console` and `node:console` imports.
Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time.
There, `%s` of a function or of a class instance whose class defines `toString` and `%o` of an object throw `SC2020`.
`util.inspect` with `colors`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles.
Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`.
Inspect options other than `depth`, `colors`, `customInspect`, `compact` (a number or `false`), `breakLength`, and `sorted` (a boolean) throw `SC2020` when they are set to a value other than Node's default.
`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them.
A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused.
Stored global console values and detached output methods retain identity and accept ordinary arguments.
Stored methods do not support format specifiers with substitution arguments.
Other stored console APIs, custom `Console` instances, and console mutation are unsupported.
+1 -1
View File
@@ -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.
@@ -25,3 +25,10 @@ Index-signature records list declared fields before the other keys.
Optional fields and properties added through a record's `unknown` view can be deleted.
A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #<Object>` and leaves the record unchanged, where Node.js removes the property.
Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`.
A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name.
A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js.
A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record<string, number>`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new.
Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record<string, (x: number) => number>`.
Other inherited names, such as `constructor`, read `undefined` from typed records.
@@ -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.
+135 -135
View File
@@ -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"
}
},
{
@@ -21382,9 +21382,9 @@
"anchor": "err_invalid_mime_syntax",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -23520,9 +23520,9 @@
"anchor": "err_throttled",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -23790,9 +23790,9 @@
"anchor": "err_tls_required_server_name",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -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"
}
},
{
@@ -71946,9 +71946,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -71969,9 +71969,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -71992,9 +71992,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72015,9 +72015,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72038,9 +72038,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72061,9 +72061,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72176,9 +72176,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72199,9 +72199,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72222,9 +72222,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72245,9 +72245,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72268,9 +72268,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72291,9 +72291,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72314,9 +72314,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72337,9 +72337,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72360,9 +72360,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -72636,9 +72636,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72751,9 +72751,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72774,9 +72774,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72797,9 +72797,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -73027,9 +73027,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -73050,9 +73050,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -73395,9 +73395,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -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"
},
@@ -25723,8 +25806,12 @@
"anchor": "dep0044-utilisarray",
"nodeStability": null,
"static": {
"status": "not-applicable",
"evidence": "compiler-chapter-policy:deprecations"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.isArray",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -26011,8 +26098,12 @@
"anchor": "dep0060-util_extend",
"nodeStability": null,
"static": {
"status": "not-applicable",
"evidence": "compiler-chapter-policy:deprecations"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util._extend",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -32679,6 +32770,9 @@
"tests": [
"tests/corpus/6702-ecdh-errors.js",
"tests/corpus/6708-dh-errors.js",
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/caught-system-error-fields.ts",
"tests/corpus/error-stack-frames.ts",
@@ -36604,8 +36698,11 @@
"anchor": "err_invalid_mime_syntax",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_INVALID_MIME_SYNTAX",
"tests": [
"tests/corpus/7707-util-mime-type.cjs"
]
},
"anchorSource": "exact"
},
@@ -38742,8 +38839,11 @@
"anchor": "err_throttled",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_THROTTLED",
"tests": [
"tests/corpus/7711-util-throttle.cjs"
]
},
"anchorSource": "exact"
},
@@ -39012,8 +39112,11 @@
"anchor": "err_tls_required_server_name",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME",
"tests": [
"tests/corpus/7440-tls-server-add-context.ts"
]
},
"anchorSource": "exact"
},
@@ -95877,6 +95980,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"
]
},
@@ -96245,8 +96359,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"
},
@@ -96372,8 +96492,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"
},
@@ -96556,8 +96682,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"
},
@@ -96699,8 +96831,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"
},
@@ -96791,8 +96928,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"
},
@@ -96860,8 +97002,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"
},
@@ -96978,8 +97125,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"
},
@@ -97119,8 +97271,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"
},
@@ -97142,8 +97300,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"
},
@@ -101420,6 +101584,25 @@
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/6428-promisify-callback-functions.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs",
"tests/corpus/7522-promisify-untyped.cjs",
"tests/corpus/7523-promisify-untyped.ts",
"tests/corpus/7524-inspect-custom.ts",
"tests/corpus/7525-inspect-custom.cjs",
"tests/corpus/7526-inspect-options-runtime.ts",
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts",
"tests/corpus/7702-util-signal-exit-code.cjs",
"tests/corpus/7703-util-aborted.cjs",
"tests/corpus/7704-util-aborted.ts",
"tests/corpus/7705-util-diff.cjs",
"tests/corpus/7706-util-diff.ts",
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts",
"tests/corpus/7710-util-debounce.cjs",
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7712-util-debounce-throttle.ts",
"tests/corpus/7747-text-codec-untyped-slot.js",
"tests/corpus/node24-deep-equality-properties.js",
"tests/corpus/node24-deep-equality.cjs",
@@ -101506,8 +101689,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util._extend",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -101529,8 +101716,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.isArray",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -101552,8 +101743,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.MIMEType",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101575,8 +101770,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mime.toString",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101598,8 +101797,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mime.toJSON",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101621,8 +101824,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.MIMEType.parse",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101736,8 +101943,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.MIMEParams",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101759,8 +101970,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.delete",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101782,8 +101997,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.entries",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101805,8 +102024,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.get",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101828,8 +102051,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.has",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101851,8 +102078,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.keys",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101874,8 +102105,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.set",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101897,8 +102132,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.values",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101920,8 +102159,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -102320,8 +102563,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.convertProcessSignalToExitCode",
"tests": [
"tests/corpus/7702-util-signal-exit-code.cjs"
]
},
"anchorSource": "exact"
},
@@ -102455,8 +102701,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.debounce",
"tests": [
"tests/corpus/7710-util-debounce.cjs",
"tests/corpus/7712-util-debounce-throttle.ts"
]
},
"anchorSource": "exact"
},
@@ -102478,8 +102728,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.throttle",
"tests": [
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7712-util-debounce-throttle.ts"
]
},
"anchorSource": "exact"
},
@@ -102501,8 +102755,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.diff",
"tests": [
"tests/corpus/7705-util-diff.cjs",
"tests/corpus/7706-util-diff.ts"
]
},
"anchorSource": "exact"
},
@@ -102529,7 +102787,9 @@
"tests": [
"tests/corpus/1636-util-format.ts",
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/5835-util-format-integer-specifiers.ts"
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs"
]
},
"anchorSource": "exact"
@@ -102557,7 +102817,9 @@
"tests": [
"tests/corpus/1636-util-format.ts",
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/5835-util-format-integer-specifiers.ts"
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs"
]
},
"anchorSource": "exact"
@@ -102748,7 +103010,8 @@
"tests/corpus/1632-inspect-records.ts",
"tests/corpus/1637-inspect-dyn.ts",
"tests/corpus/2485-inspect-circular-refs.ts",
"tests/corpus/5884-inspect-layout-options.ts"
"tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/7526-inspect-options-runtime.ts"
]
},
"anchorSource": "exact"
@@ -102771,8 +103034,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.inspect",
"tests": [
"tests/corpus/7526-inspect-options-runtime.ts"
]
},
"anchorSource": "exact"
},
@@ -102794,8 +103060,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.custom",
"tests": [
"tests/corpus/7524-inspect-custom.ts",
"tests/corpus/7525-inspect-custom.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -103112,6 +103382,8 @@
"tests/fixtures/node-types/promisify-dns.ts",
"tests/corpus/6428-promisify-callback-functions.ts",
"tests/fixtures/node-types/promisify-callback-functions.ts",
"tests/corpus/7522-promisify-untyped.cjs",
"tests/corpus/7523-promisify-untyped.ts",
"tests/harness/project-config.test.ts"
]
},
@@ -103327,8 +103599,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.aborted",
"tests": [
"tests/corpus/7703-util-aborted.cjs",
"tests/corpus/7704-util-aborted.ts"
]
},
"anchorSource": "exact"
},
+24 -3
View File
@@ -187,6 +187,13 @@
"node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"],
"node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"],
"node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"],
"node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
"node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
"node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"],
"node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"],
"node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"],
"node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
"node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
@@ -208,6 +215,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"] },
@@ -251,9 +260,11 @@
{"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
{"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]},
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] },
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] },
{ "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] },
{ "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] },
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
{ "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" },
@@ -263,6 +274,10 @@
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
{ "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
{ "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] },
{ "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] },
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
@@ -407,7 +422,7 @@
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
@@ -436,6 +451,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"] },
@@ -446,6 +466,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"] },
+165 -9
View File
@@ -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;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
"version": "0.4.4",
"version": "0.4.5",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "scriptc",
"version": "0.4.4",
"version": "0.4.5",
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+2 -2
View File
@@ -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: 45, marker: "scr_runtime_abi_v45" },
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_v45");
});
});
+3 -1
View File
@@ -50,9 +50,11 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
const baseline = await build([]);
expect(baseline).not.toContain('"target-cpu"');
// Only --sanitize builds mark the functions for ASan instrumentation.
expect(baseline).not.toContain("sanitize_address");
expect(await build(["--target-cpu=baseline"])).toBe(baseline);
const v3 = await build(["--target-cpu=x86-64-v3"]);
expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected);
// The bootstrap's cached route builds without a level.
expect(await build([], true)).toBe(baseline);
+272 -22
View File
@@ -2914,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;
@@ -2938,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;
@@ -2974,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",
@@ -3007,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",
@@ -3020,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",
@@ -3551,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
@@ -3910,6 +3961,85 @@ declare module "util" {
): string;
export function parseEnv(content: string): { [key: string]: string | undefined };
export function inherits(constructor: unknown, superConstructor: unknown): void;
export class MIMEType {
constructor(input: string | { toString: () => string });
static parse(input: string | { toString: () => string }): MIMEType | null;
type: string;
subtype: string;
readonly essence: string;
readonly params: MIMEParams;
toString(): string;
toJSON(): string;
}
export class MIMEParams {
delete(name: string): void;
entries(): IterableIterator<[name: string, value: string]>;
get(name: string): string | null;
has(name: string): boolean;
keys(): IterableIterator<string>;
set(name: string, value: string): void;
values(): IterableIterator<string>;
toString(): string;
toJSON(): string;
[Symbol.iterator](): IterableIterator<[name: string, value: string]>;
}
export type DiffEntry = [operation: -1 | 0 | 1, value: string];
export function diff(
actual: string | readonly string[],
expected: string | readonly string[],
): DiffEntry[];
export interface DebounceOptions {
leading?: boolean;
rejectOnCancel?: boolean;
signal?: AbortSignal;
}
/** Every call answers a promise of the result of the call it waits for. */
export interface DebouncedFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
flush(): void;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
}
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
options?: DebounceOptions,
): DebouncedFunction<T>;
export interface ThrottleOptions {
concurrency?: number;
maxPending?: number;
overflow?: "queue" | "drop";
signal?: AbortSignal;
strict?: boolean;
}
export interface ThrottledFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
hasImmediateCapacity(): boolean;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
readonly activeCount: number;
}
export function throttle<T extends (...args: any[]) => any>(
fn: T,
limit: number,
interval: number,
options?: ThrottleOptions,
): ThrottledFunction<T>;
/** Resolves with the abort event once `signal` aborts, or with undefined
* when it already has. */
export function aborted(signal: AbortSignal, resource: any): Promise<unknown>;
/** 128 plus the number of the signal, the exit code of a process it terminates. */
export function convertProcessSignalToExitCode(signalCode: string): number;
/** @deprecated Since v4.0.0 - Use `Array.isArray()` instead. */
export function isArray(object: unknown): object is unknown[];
/** @deprecated Since v6.0.0 - Use `Object.assign()` instead. Answers target itself. */
export function _extend<T extends object>(target: T, source: object | null | undefined): T;
export interface DeprecateOptions {
modifyPrototype?: boolean;
}
@@ -3967,7 +4097,13 @@ declare module "util" {
export type TextDecoder = globalThis.TextDecoder;
export var TextEncoder: typeof globalThis.TextEncoder;
export var TextDecoder: typeof globalThis.TextDecoder;
/* stream.pipeline's overload comes first: its __promisify__ member keeps
/* An untyped function: an `any` value, or a function whose parameters
* are untyped (JavaScript's `(value, callback) => ...`), the only
* parameters that accept this object type. The promisified function is
* untyped too. It comes first because such functions match every other
* overload. */
export function promisify(fn: (...args: { readonly "scriptc:untyped": true }[]) => unknown): any;
/* stream.pipeline's overload comes next: its __promisify__ member keeps
* every other target out of it. */
export function promisify(
fn: typeof import("node:stream").pipeline,
@@ -4088,15 +4224,56 @@ declare module "util" {
): (address: string, port: number) => Promise<{ hostname: string; service: string }>;
/* Declared surface without full lowering (the type-level tolerance
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
* without a lowering fences at its site). inspect's options carry the
* documented knobs through an index signature. */
* without a lowering fences at its site). */
export interface InspectOptions {
showHidden?: boolean | undefined;
depth?: number | null | undefined;
colors?: boolean | undefined;
customInspect?: boolean | undefined;
showProxy?: boolean | undefined;
maxArrayLength?: number | null | undefined;
maxStringLength?: number | null | undefined;
breakLength?: number | undefined;
compact?: boolean | number | undefined;
sorted?: boolean | ((a: string, b: string) => number) | undefined;
getters?: "get" | "set" | boolean | undefined;
numericSeparator?: boolean | undefined;
}
export type Style =
| "special"
| "number"
| "bigint"
| "boolean"
| "undefined"
| "null"
| "string"
| "symbol"
| "date"
| "regexp"
| "module";
export interface InspectOptionsStylized extends InspectOptions {
stylize(text: string, styleType: Style): string;
}
export type CustomInspectFunction = (
depth: number,
options: InspectOptionsStylized,
inspect: typeof import("node:util").inspect,
) => any;
export function inspect(
object: unknown,
options?: { depth?: number | null; colors?: boolean; [k: string]: unknown },
showHidden?: boolean,
depth?: number | null,
color?: boolean,
): string;
export function inspect(object: unknown, options?: InspectOptions): string;
export namespace inspect {
/* The key of an object's custom inspect function:
* Symbol.for('nodejs.util.inspect.custom'). */
const custom: unique symbol;
}
export function format(format?: unknown, ...args: unknown[]): string;
export function formatWithOptions(
inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown },
inspectOptions: InspectOptions,
format?: unknown,
...args: unknown[]
): string;
@@ -5294,10 +5471,10 @@ declare module "node:zlib" {
* with the real port discoverable as address().port (the composed read
* is the lowered form — bare address() has no lowering); close([cb])
* firing once connections drain; socket write/end/destroy/pipe and the
* data/end/close/error/connect events (on and once — listeners take at
* most the declared parameter). write/end return void here (Node returns
* the backpressure boolean / the socket); on/once return void (chaining
* is fenced). connect's host is a numeric IP or "localhost" (pinned to
* data/end/close/error/connect/drain events (on and once — listeners
* take at most the declared parameter). write returns Node's
* backpressure boolean; end returns void here (Node returns the socket);
* on/once return void (chaining is fenced). connect's host is a numeric IP or "localhost" (pinned to
* 127.0.0.1 — SEMANTICS.md); every failure is the async 'error' event,
* and an 'error' with no listener exits 1 like an unhandled EventEmitter
* 'error'. */
@@ -5329,7 +5506,13 @@ declare module "net" {
readonly bytesWritten: number;
/* write's callback runs once the data is flushed, with null; end's
* once the socket finishes. */
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void;
/* false when the bytes this write left unsent reach the 65536-byte
* high-water mark, as in Node; 'drain' fires when they are all sent. */
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): boolean;
/* the bytes written and not yet sent */
readonly writableLength: number;
/* a write answered false and 'drain' has not fired yet */
readonly writableNeedDrain: boolean;
end(callback?: () => void): void;
end(data: string | Uint8Array, callback?: () => void): void;
destroy(): void;
@@ -5377,6 +5560,7 @@ declare module "net" {
| "connect"
| "ready"
| "timeout"
| "drain"
| "readable"
| "finish"
| "secureConnect"
@@ -5401,6 +5585,7 @@ declare module "net" {
| "connect"
| "ready"
| "timeout"
| "drain"
| "readable"
| "finish"
| "secureConnect"
@@ -5424,6 +5609,7 @@ declare module "net" {
| "connect"
| "ready"
| "timeout"
| "drain"
| "readable"
| "finish"
| "secureConnect"
@@ -5473,6 +5659,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;
@@ -5492,6 +5683,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;
@@ -5505,6 +5701,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;
@@ -5958,8 +6159,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 —
@@ -5993,7 +6201,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,
@@ -6048,6 +6296,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
+21
View File
@@ -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.
+13
View File
@@ -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);
}
+33
View File
@@ -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;
+251 -7
View File
@@ -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" }
]
}
+19 -1
View File
@@ -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
+101 -9
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.4.4",
"version": "0.4.5",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -20,6 +20,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
@@ -33,6 +34,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.declAdopt ? { declAdopt: true } : {}),
...(features.dynForAwait ? { dynForAwait: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.mime ? { mime: true } : {}),
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
// Island code's signal and exit listeners join the events unit's
@@ -65,6 +68,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`)),
)),
);
}
+13 -1
View File
@@ -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) {
+61
View File
@@ -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([]);
},
);
+49
View File
@@ -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;
}
}
+13 -1
View File
@@ -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)}`;
}
}
@@ -9,17 +9,29 @@ import type { LlvmEmitterContext } from "./expr-context.js";
* receiver slots loop-invariant. Untagged accesses and calls still alias
* every category.
*
* Header loads are also `!invariant.load`: no store or call between the
* construction of a view and its release changes them. Without it, a data
* pointer load ahead of a bounds branch is sunk into the in-bounds block,
* and LICM keeps a conditionally executed load inside the loop. */
* Header loads that only bound or address an access to the view are also
* `!invariant.load`: no store or call between the construction of a view
* and its release changes them, and their values reach only that access,
* which runs while the view is alive. Without it, a data pointer load
* ahead of a bounds branch is sunk into the in-bounds block, and LICM keeps
* a conditionally executed load inside the loop. */
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
const tag = host.memoryAliasAttachment("bytes:header");
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
}
/** A header load whose value the program reads (`length`, `byteLength`)
* keeps the alias tag only. The value can outlive the view, and LLVM may
* move an invariant load to its use, past the release that frees the
* view. */
export function bytesHeaderValueAlias(host: LlvmEmitterContext): string {
return host.memoryAliasAttachment("bytes:header");
}
/** Element storage's tag, and the alias scope of the access being emitted
* in a loop version's fast copy (bytesAccessScope). */
export function bytesElementAlias(host: LlvmEmitterContext): string {
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 {
@@ -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;
@@ -453,7 +453,11 @@ export function emitDenseArrayRead(
B.br(join);
B.startBlock(slow);
const slowValue = B.tmp();
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
// ToNumber of a foreign element (SCR_ARR_FOREIGN) can run user code.
const attrs =
(acc === "ref" && retain) || (getter === "scr_arr_get_number" && host.mayHoldForeign(element))
? " cold"
: READ_FALLBACK;
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
let slowInt: string | null = null;
if (fastInt !== null) {
@@ -610,6 +614,28 @@ export function emitDenseArraySet(
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
};
if ((acc === "f64" || kind === "ref") && host.mayHoldForeign(element)) {
// Past the hole-free prefix a slot may hold a foreign element, whose
// value the runtime setter releases; below it every slot is a value.
B.br(slow);
B.startBlock(filledLabel);
const filledValuePtr = valuePointer(host, guard, kind);
let filledOld: string | null = null;
if (kind === "ref") {
filledOld = B.tmp();
B.line(
`${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`,
);
}
storeValue(filledValuePtr);
if (filledOld) host.releaseHeap(filledOld, element);
B.br(join);
B.startBlock(slow);
call(" cold");
B.br(join);
B.startBlock(join);
return;
}
const stated = B.newLabel("arr.set.states"),
plain = B.newLabel("arr.set.plain"),
plainStore = B.newLabel("arr.set.values"),
@@ -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();
+396 -41
View File
@@ -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[];
}
@@ -388,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
@@ -455,6 +537,7 @@ export class LlDyn {
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 =>
@@ -494,7 +577,7 @@ export class LlDyn {
positions.forEach((position, index) => {
const tag = order[position]!;
const failed = B.newLabel("du.c.f");
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs);
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();
@@ -517,12 +600,23 @@ export class LlDyn {
tryCase(-1);
order.forEach((tag, position) => {
B.startBlock(from[position]!);
this.unionArmMatch(B, def, tag, hits[tag]!, after(position), preserveRefs);
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,
@@ -530,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) {
@@ -558,10 +653,12 @@ export class LlDyn {
});
B.startBlock(structure);
}
this.unionArmStructure(B, def, tag, yes, no, preserveRefs);
this.unionArmStructure(B, def, tag, yes, no, preserveRefs, trustedKind);
}
/** The part of unionArmMatch past the arm's literal test. */
/** 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,
@@ -569,7 +666,21 @@ export class LlDyn {
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)`,
@@ -667,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();
@@ -1053,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;
@@ -1368,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})`);
@@ -1379,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");
@@ -1474,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":
@@ -1502,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.
@@ -1613,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}`);
}
@@ -1630,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;
@@ -1649,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[] = [];
@@ -1693,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;
@@ -1745,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;
@@ -1789,8 +1980,90 @@ export class LlDyn {
/** `sc_dc_<n>(ptr d, ptr path) -> T` — validate the checked-dynamic tree against T and
* BUILD the typed value (+1), or throw the catchable path-annotated
* TypeError and return a dummy with the pending flag set. */
/** The array a live view's snapshot commits back into, for element types
* whose arrays can hold foreign elements (LlEmitter.mayHoldForeign):
* like dynCheckHelper's array case on a checked array `%d`, except that
* an element the element type cannot hold is stored as a foreign element
* (undefined as a present undefined) instead of failing the conversion.
* Boolean arrays, whose slots cannot hold one, keep the failing check. */
dynViewArrayHelper(t: IrType & { kind: "array" }): string {
const builderKey = `view:${typeKey(t)}`;
const existing = this.dynBuilders.get(builderKey);
if (existing) return existing;
const name = `sc_dva_${this.dynBuilders.size}`;
this.dynBuilders.set(builderKey, name);
const host = this.host;
const elem = t.elem;
const check = this.dynCheckHelper(elem);
const match = this.dynMatchHelper(elem);
const B = new BlockBuilder();
const n = this.lenOf(B, "%d");
const a = B.tmp();
B.line(`${a} = ${arrNewCall(host, elem, n)}`);
const items = this.itemsOf(B, "%d");
const acc = elemAccess(elem);
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
host.declare(
`declare double @scr_arr_push_${acc}(ptr, ${acc === "bool" ? "i1 zeroext" : accTy})`,
);
host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
host.declare(`declare void @scr_arr_set_len(ptr, double)`);
host.declare(`declare zeroext i1 @scr_arr_set_foreign(ptr, double, ptr)`);
this.i64Loop(B, "dva", n, (i) => {
const index = B.tmp();
B.line(`${index} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
const present = B.tmp();
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
const valueLabel = B.newLabel("dva.present");
const holeLabel = B.newLabel("dva.hole");
const typedLabel = B.newLabel("dva.typed");
const foreignLabel = B.newLabel("dva.foreign");
const doneLabel = B.newLabel("dva.done");
B.condBr(present, valueLabel, holeLabel);
B.startBlock(holeLabel);
const length = B.tmp();
B.line(`${length} = fadd double ${index}, 1.0`);
B.line(`call void @scr_arr_set_len(ptr ${a}, double ${length})`);
B.br(doneLabel);
B.startBlock(valueLabel);
const e = this.itemAt(B, items, i);
const fits = B.tmp();
B.line(`${fits} = call zeroext i1 @${match}(ptr ${e})`);
B.condBr(fits, typedLabel, foreignLabel);
B.startBlock(foreignLabel);
const kept = B.tmp();
B.line(`${kept} = call zeroext i1 @scr_arr_set_foreign(ptr ${a}, double ${index}, ptr ${e})`);
B.condBr(kept, doneLabel, typedLabel);
B.startBlock(typedLabel);
const v = B.tmp();
B.line(`${v} = call ${this.valTy(elem)} @${check}(ptr ${e}, ptr null)`);
this.pendingBail(
B,
"dva",
() => {
host.declare(`declare void @scr_arr_release(ptr)`);
B.line(`call void @scr_arr_release(ptr ${a})`);
},
"ptr null",
);
const pushed = B.tmp();
B.line(`${pushed} = call double @scr_arr_push_${acc}(ptr ${a}, ${accTy} ${v})`);
B.br(doneLabel);
B.startBlock(doneLabel);
});
B.terminate(`ret ptr ${a}`);
this.defs.push(
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} { ; view commit ${typeKey(t)}`,
B.render(),
`}`,
``,
);
return name;
}
dynCheckHelper(t: IrType, preserveRefs = false): string {
const key = typeKey(t);
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
const key = checkedConversionKey(t);
const builderKey = preserveRefs ? `identity:${key}` : key;
const existing = this.dynBuilders.get(builderKey);
if (existing) return existing;
@@ -1808,6 +2081,26 @@ export class LlDyn {
const B = new BlockBuilder();
if (t.kind === "classval") {
host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`);
// A subclass's class value checks into the base class's slot when
// construction through the slot can call its thunk with the base
// constructor's arguments; any other subclass fails the check.
const subclasses = host.classValueSubtypes(t.className);
if (subclasses.length > 0) {
host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
for (const className of subclasses) {
const subKey = host.cstr(typeKey({ kind: "classval", className }));
const matched = B.tmp();
B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`);
const yes = B.newLabel("sub.yes");
const no = B.newLabel("sub.no");
B.condBr(matched, yes, no);
B.startBlock(yes);
const sub = B.tmp();
B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`);
B.terminate(`ret ptr ${sub}`);
B.startBlock(no);
}
}
const checked = B.tmp();
B.line(
`${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`,
@@ -1825,11 +2118,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} {`,
@@ -2570,6 +2870,21 @@ export class LlDyn {
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
const e = this.itemAt(B, items, i);
if (t.optionalElements) {
// An undefined element is a present undefined element.
const undef = B.tmp();
const storeUndefined = B.newLabel("dca.undef");
const checkLabel = B.newLabel("dca.check");
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
B.condBr(undef, storeUndefined, checkLabel);
B.startBlock(storeUndefined);
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
const at = B.tmp();
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
B.br(doneLabel);
B.startBlock(checkLabel);
}
const v = B.tmp();
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
this.pendingBail(
@@ -2596,7 +2911,18 @@ 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.
// 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]!;
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
@@ -2624,14 +2950,22 @@ 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)})`,
);
@@ -2652,6 +2986,7 @@ export class LlDyn {
unmatched,
preserveRefs,
`@${name}.cases`,
trustedKinds,
);
for (const i of order) {
B.startBlock(hits[i]!);
@@ -2662,7 +2997,7 @@ export class LlDyn {
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);
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!);
B.startBlock(lHit);
build(i);
B.startBlock(lNext);
@@ -2917,14 +3252,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);
@@ -2939,10 +3274,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();
@@ -2953,11 +3290,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}`);
@@ -3137,7 +3474,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;
@@ -3430,7 +3767,25 @@ export class LlDyn {
B.br(doneLabel);
B.startBlock(presentLabel);
B.line(`${undefinedState} = fcmp oeq double ${state}, 2.0`); // SCR_ARR_UNDEFINED
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
if (host.mayHoldForeign(t.elem)) {
// A foreign element (SCR_ARR_FOREIGN) holds its own value.
const typedLabel = B.newLabel("tda.typed");
const foreignLabel = B.newLabel("tda.foreign");
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${typedLabel}`);
B.startBlock(typedLabel);
const foreign = B.tmp();
B.line(`${foreign} = fcmp oeq double ${state}, 3.0`);
B.condBr(foreign, foreignLabel, valueLabel);
B.startBlock(foreignLabel);
host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`);
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
const payload = B.tmp();
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %v, double ${i}) ; +1`);
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${payload})`);
B.br(doneLabel);
} else {
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
}
B.startBlock(undefinedLabel);
const retained = this.retainDyn(B, this.undef(B));
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${retained})`);
@@ -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;
@@ -187,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,
@@ -596,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]`);
}
@@ -636,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);
@@ -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>,
+220 -130
View File
@@ -63,7 +63,7 @@ import { nodeIslandManifest } from "../../node-islands/modules.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
import { literalEqualityDefinition } from "./string-literal-equality.js";
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
import { InternalCompilerError } from "../../errors.js";
import { targetCpuAttribute } from "../target-cpu.js";
@@ -104,10 +104,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;
}
@@ -136,6 +136,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] |
let code = 1;
try {
emitter.journal = new EmitJournal();
emitter.debug?.record();
const texts: string[] = [];
for (const fn of functions.slice(range * size, (range + 1) * size)) {
texts.push(emitter.emitFunction(fn));
@@ -197,15 +198,17 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
/** The attribute groups of the emitted functions: #0 for every function,
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
* the build's `--target-cpu` level. */
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
* builds. A group needs one attribute: without either, #0 holds a string
* attribute that LLVM ignores. */
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
const cpu = targetCpuAttribute(emitter.targetCpu);
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
const base = `${asan}${cpu}` || ` "scriptc-function"`;
return [
`attributes #0 = { sanitize_address${cpu} }`,
...(emitter.usesCoroutines
? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`]
: []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []),
`attributes #0 = {${base} }`,
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
];
}
@@ -232,13 +235,10 @@ 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) {
@@ -620,6 +620,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
regexMatchers.set(key, code);
for (const decl of code.decls) emitter.declare(decl);
}
// Replacers of global substitutions with a literal replacement; the call
// sites register only patterns inside the compiled subset.
const regexReplacers: RegexMatcherCode[] = [];
for (const rr of emitter.regexReplacers.values()) {
const code = compileRegexMatcher(
rr.sym,
rr.pattern,
rr.flags,
emitter.sizeType,
!emitter.wasi,
rr.replacement,
);
if (code === null)
throw new InternalCompilerError(`llvm emitter bug: replacer ${rr.sym} did not compile`);
regexReplacers.push(code);
for (const decl of code.decls) emitter.declare(decl);
}
const out: string[] = [
`; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`,
@@ -903,6 +920,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
}
if (emitter.regexInstances.size > 0) out.push(``);
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
for (const code of regexReplacers) out.push(...code.lines, ``);
for (const [, inst] of emitter.templateStringsInstances) {
// One immortal ScrArr per tagged-template site: a data global of
// interned cooked-string literals, and the ScrArr header over it
@@ -1033,7 +1051,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());
@@ -1185,10 +1203,12 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
: [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]),
`}`,
``,
...emitter.inlineRcTail(flushedDecls),
// sanitize_address is inert under the plain pipeline; the sanitized
// lane's -fsanitize=address link activates instrumentation over the
// emitted functions too (the runtime TUs get theirs from clang).
);
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
out.push(
// The sanitized lane's -fsanitize=address link instruments the
// functions marked sanitize_address (the runtime TUs get theirs from
// clang).
...functionAttributes(emitter, hasNoInlineRecordClone),
``,
);
@@ -1217,6 +1237,126 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
return out;
}
/** The out-of-line hashed probe of a number-key map (scr_map_find_f64):
* `keyLines` define the normalized key word %kb from the parameter, and
* `runtimeKey` is the double passed to the runtime lookup. A map with at
* most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key
* filter is tested before its entries are scanned. A bucket table is
* probed without the filter: the filter keeps two of its 64 bits per key,
* so past a few dozen keys it rejects few absent keys, and the probe's
* first bucket read answers those. Compact tables tag each 24-bit entry
* index (+1) with the hash's top byte; wider tables take the runtime
* lookup. */
function hashedNumberProbe(
name: string,
param: string,
sz: string,
keyLines: string[],
runtimeKey: string,
): string[] {
return [
`define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`,
`entry:`,
...keyLines,
` %x1s = lshr i64 %kb, 30`,
` %x1 = xor i64 %kb, %x1s`,
` %x2 = mul i64 %x1, -4658895280553007687`,
` %x3s = lshr i64 %x2, 27`,
` %x3 = xor i64 %x2, %x3s`,
` %x4 = mul i64 %x3, -7723592293110705685`,
` %x5s = lshr i64 %x4, 31`,
` %x5 = xor i64 %x4, %x5s`,
` %hz = icmp eq i64 %x5, 0`,
` %h = select i1 %hz, i64 1, i64 %x5`,
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
...(sz === "i64"
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
: [` %nb = load i32, ptr %nbp`]),
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %ents = load ptr, ptr %eps`,
` %linear = icmp eq ${sz} %nb, 0`,
` br i1 %linear, label %filter, label %buckets`,
`filter:`,
` %f1s = lshr i64 %h, 44`,
` %f1a = and i64 %f1s, 63`,
` %f1 = shl i64 1, %f1a`,
` %f2s = lshr i64 %h, 50`,
` %f2a = and i64 %f2s, 63`,
` %f2 = shl i64 1, %f2a`,
` %fb = or i64 %f1, %f2`,
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter.bits = load i64, ptr %fp`,
` %fm = and i64 %filter.bits, %fb`,
` %fin = icmp eq i64 %fm, %fb`,
` br i1 %fin, label %scan, label %miss`,
// A number-key entry has no cached hash; a dead entry's key matches no key.
`scan:`,
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
...(sz === "i64"
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
: [` %ne = load i32, ptr %nep`]),
` br label %scan.loop`,
`scan.loop:`,
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
` %sdone = icmp eq ${sz} %si, %ne`,
` br i1 %sdone, label %miss, label %scan.entry`,
`scan.entry:`,
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
` %sk = load i64, ptr %se`,
` %skq = icmp eq i64 %sk, %kb`,
` br i1 %skq, label %scan.found, label %scan.next`,
`scan.found:`,
` ret ptr %se`,
`scan.next:`,
` %si.next = add ${sz} %si, 1`,
` br label %scan.loop`,
`buckets:`,
` %wide = icmp ugt ${sz} %nb, 16777216`,
` br i1 %wide, label %runtime, label %probe`,
`probe:`,
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
` %bk = load ptr, ptr %bp`,
` %mask = sub ${sz} %nb, 1`,
` %ht = lshr i64 %h, 32`,
` %ht32 = trunc i64 %ht to i32`,
` %tag = and i32 %ht32, -16777216`,
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
` %i0 = and ${sz} %hi, %mask`,
` br label %probe.loop`,
`probe.loop:`,
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
` %b = load i32, ptr %bep`,
` %bempty = icmp eq i32 %b, 0`,
` br i1 %bempty, label %miss, label %probe.tag`,
`probe.tag:`,
` %bt = and i32 %b, -16777216`,
` %btq = icmp eq i32 %bt, %tag`,
` br i1 %btq, label %probe.entry, label %probe.next`,
`probe.entry:`,
` %bix = and i32 %b, 16777215`,
` %bix1 = sub i32 %bix, 1`,
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
` %pk = load i64, ptr %pe`,
` %pkq = icmp eq i64 %pk, %kb`,
` br i1 %pkq, label %probe.found, label %probe.next`,
`probe.found:`,
` ret ptr %pe`,
`probe.next:`,
` %pi.inc = add ${sz} %pi, 1`,
` %pi.next = and ${sz} %pi.inc, %mask`,
` br label %probe.loop`,
`miss:`,
` ret ptr null`,
`runtime:`,
` %r = call ptr @scr_map_entry_f64(ptr %m, double ${runtimeKey}) memory(read) nounwind willreturn`,
` ret ptr %r`,
`}`,
``,
];
}
/** The shared abort helpers (emitted only when referenced): the OOM
* abort of untraced shape allocation and the invalid-union-tag abort —
* both print the runtime ABI’s exact message on fd 2 and abort. */
@@ -1351,114 +1491,66 @@ export function helperDefs(emitter: LlEmitter): string[] {
` ret ptr %hashed`,
`}`,
``,
// Hashing (scr_map_find_f64): the SameValueZero key word, its
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
// The same lookup for a key the emitter knows is an integer (`%ki`,
// the double `%k`'s exact value): the direct index tests the integer
// itself, so the double is converted only by the hashed probe. A
// negative key is not below the index's size as an unsigned value.
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
`entry:`,
` %nan = fcmp uno double %k, %k`,
` %zero = fcmp oeq double %k, 0.0`,
` %kz = select i1 %zero, double 0.0, double %k`,
` %kzb = bitcast double %kz to i64`,
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
` %x1s = lshr i64 %kb, 30`,
` %x1 = xor i64 %kb, %x1s`,
` %x2 = mul i64 %x1, -4658895280553007687`,
` %x3s = lshr i64 %x2, 27`,
` %x3 = xor i64 %x2, %x3s`,
` %x4 = mul i64 %x3, -7723592293110705685`,
` %x5s = lshr i64 %x4, 31`,
` %x5 = xor i64 %x4, %x5s`,
` %hz = icmp eq i64 %x5, 0`,
` %h = select i1 %hz, i64 1, i64 %x5`,
` %f1s = lshr i64 %h, 44`,
` %f1a = and i64 %f1s, 63`,
` %f1 = shl i64 1, %f1a`,
` %f2s = lshr i64 %h, 50`,
` %f2a = and i64 %f2s, 63`,
` %f2 = shl i64 1, %f2a`,
` %fb = or i64 %f1, %f2`,
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter = load i64, ptr %fp`,
` %fm = and i64 %filter, %fb`,
` %fin = icmp eq i64 %fm, %fb`,
` br i1 %fin, label %table, label %miss`,
`table:`,
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
...(sz === "i64"
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
: [` %nb = load i32, ptr %nbp`]),
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %ents = load ptr, ptr %eps`,
` %linear = icmp eq ${sz} %nb, 0`,
` br i1 %linear, label %scan, label %buckets`,
// Small maps keep no bucket table: scan the entries' keys. A number-key
// entry has no cached hash; a dead entry's key matches no key.
`scan:`,
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
...(sz === "i64"
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
: [` %ne = load i32, ptr %nep`]),
` br label %scan.loop`,
`scan.loop:`,
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
` %sdone = icmp eq ${sz} %si, %ne`,
` br i1 %sdone, label %miss, label %scan.entry`,
`scan.entry:`,
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
` %sk = load i64, ptr %se`,
` %skq = icmp eq i64 %sk, %kb`,
` br i1 %skq, label %scan.found, label %scan.next`,
`scan.found:`,
` ret ptr %se`,
`scan.next:`,
` %si.next = add ${sz} %si, 1`,
` br label %scan.loop`,
// Compact tables tag each 24-bit entry index (+1) with the hash's
// top byte; wider tables take the runtime lookup.
`buckets:`,
` %wide = icmp ugt ${sz} %nb, 16777216`,
` br i1 %wide, label %runtime, label %probe`,
`probe:`,
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
` %bk = load ptr, ptr %bp`,
` %mask = sub ${sz} %nb, 1`,
` %ht = lshr i64 %h, 32`,
` %ht32 = trunc i64 %ht to i32`,
` %tag = and i32 %ht32, -16777216`,
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
` %i0 = and ${sz} %hi, %mask`,
` br label %probe.loop`,
`probe.loop:`,
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
` %b = load i32, ptr %bep`,
` %bempty = icmp eq i32 %b, 0`,
` br i1 %bempty, label %miss, label %probe.tag`,
`probe.tag:`,
` %bt = and i32 %b, -16777216`,
` %btq = icmp eq i32 %bt, %tag`,
` br i1 %btq, label %probe.entry, label %probe.next`,
`probe.entry:`,
` %bix = and i32 %b, 16777215`,
` %bix1 = sub i32 %bix, 1`,
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
` %pk = load i64, ptr %pe`,
` %pkq = icmp eq i64 %pk, %kb`,
` br i1 %pkq, label %probe.found, label %probe.next`,
`probe.found:`,
` ret ptr %pe`,
`probe.next:`,
` %pi.inc = add ${sz} %pi, 1`,
` %pi.next = and ${sz} %pi.inc, %mask`,
` br label %probe.loop`,
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
` %nd32 = load i32, ptr %ndp`,
` %nd = zext i32 %nd32 to i64`,
` %inside = icmp ult i64 %ki, %nd`,
` br i1 %inside, label %slot, label %outside`,
`slot:`,
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
` %dense = load ptr, ptr %dp`,
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
` %s = load i32, ptr %sp`,
` %empty = icmp eq i32 %s, 0`,
` br i1 %empty, label %miss, label %hit`,
`hit:`,
` %e32 = sub i32 %s, 1`,
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
` ret ptr %found`,
`miss:`,
` ret ptr null`,
`runtime:`,
` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`,
` ret ptr %r`,
`outside:`,
` %dense.any = icmp ne i64 %nd, 0`,
` br i1 %dense.any, label %miss, label %slow`,
`slow:`,
` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`,
` ret ptr %hashed`,
`}`,
``,
// Hashing (scr_map_find_f64): the SameValueZero key word and its
// scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer
// view to the key word, which is never NaN or -0, and so skips the
// normalization.
...hashedNumberProbe(
"sc_map_entry_f64_hashed",
"double %k",
sz,
[
` %nan = fcmp uno double %k, %k`,
` %zero = fcmp oeq double %k, 0.0`,
` %kz = select i1 %zero, double 0.0, double %k`,
` %kzb = bitcast double %kz to i64`,
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
],
"%k",
),
...hashedNumberProbe(
"sc_map_entry_f64_hashed_int",
"i64 %ki",
sz,
[` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`],
"%kf",
),
);
}
// Map/Set iteration steps read the entries array inline: an index is
@@ -1667,9 +1759,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;
@@ -1693,8 +1783,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";
@@ -1824,7 +1914,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,
@@ -50,6 +50,7 @@ import {
matchIntegerBytesForLoop,
matchIntegerCountedForLoop,
matchIntegerPairedForLoop,
steppedLoopCounter,
} from "../../ir/integer-loops.js";
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
import {
@@ -61,6 +62,7 @@ import {
import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitCountedStart,
emitPairedLoopSafety,
} from "./counted-loops.js";
import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js";
@@ -90,6 +92,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 +107,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 +480,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 +589,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 +1014,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 +1038,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 +1092,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 +1132,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 +1148,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 +1165,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 +1213,90 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.line(`store ptr ${fresh}, ptr ${slot}`);
}
};
const workerBudget = emitter.workerLoopBudget(s);
// A counted loop over typed arrays, or a loop to a typed array's
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
// of counting a budget in its body: the inner loop over a block then
// has no exit besides its own bound, which LLVM can vectorize. A typed
// array's length is fixed, so each block can take it as its limit when
// the loop does not assign the array's binding.
//
// Inside another loop of this function, which polls at least once
// every WORKER_POLL_BLOCK of its own iterations, a loop needs less:
// - a loop whose counters' ranges bound its runs, and those of the
// loops in its body, to WORKER_POLL_BLOCK^2 needs no poll;
// - an innermost counted loop is strip-mined, typed arrays or not,
// and does not poll after its last block. A run of at most
// WORKER_POLL_BLOCK iterations then never polls, and the countdown
// leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations
// is one block, polled once as it leaves.
const enclosed =
emitter.mod.workers === true &&
emitter.jumpTargets.some((target) => target.kind === "loop");
const bounded =
enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK;
const innermost = enclosed && !containsLoop(s.body);
const stripMined =
!bounded &&
capturedLets.length === 0 &&
integerSlot !== null &&
((countedLoop !== null &&
countedLimit !== null &&
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
emitter.workerLoopPolls(s) &&
(loopAccessesTypedArrays(s) || (countedLoop !== null && innermost));
const workerBudget = stripMined || bounded ? null : emitter.workerLoopBudget(s);
// One slot for every copy of the loop: a speculative copy resumes an
// iteration in another copy's body, which then reads this bound.
let blockBound: string | null = null;
let runSlot: string | null = null;
if (stripMined) {
blockBound = B.slot();
B.entryAllocas.push(
`${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`,
);
if (innermost) {
runSlot = B.slot();
B.entryAllocas.push(`${runSlot} = alloca i1 ; cancellation poll after the run`);
}
}
freshenBindings();
emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => {
if (stripMined) {
const counter: StripMinedCounter = countedLoop
? {
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, runSlot);
return;
}
const lc = B.newLabel("loop.c");
const lb = B.newLabel("loop.b");
const lu =
@@ -2049,7 +2144,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 +2157,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 +2234,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 +2327,7 @@ export function emitLoopCopies(
const plan =
allowed &&
emitter.speculativeLoopsEnabled &&
emitter.loopCopiesEnabled &&
emitter.speculation === null &&
emitter.speculationResume === null
? planSpeculativeLoop(
@@ -2136,6 +2344,7 @@ export function emitLoopCopies(
);
},
(id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id),
emitter.speculationContext(),
)
: null;
if (!plan) {
@@ -2147,6 +2356,7 @@ export function emitLoopCopies(
const kinds =
allowed &&
emitter.speculativeLoopsEnabled &&
emitter.loopCopiesEnabled &&
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
emitter.int32Kinds === null &&
emitter.speculation === null &&
@@ -2193,6 +2403,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 +2442,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 +2451,7 @@ export function emitLoopCopies(
emitCopy(exit);
} finally {
emitter.speculationResume = null;
emitter.speculationResumeValues = null;
emitter.speculationHeader = null;
}
B.startBlock(fast);
@@ -2274,6 +2500,7 @@ export function emitLoopCopies(
deferred,
bails,
ran,
values: plan.values,
};
try {
const leave = B.newLabel("spec.leave");
@@ -2421,6 +2648,218 @@ function emitInt32KindLoop(
/** At the top of a loop copy's header: lets a speculative loop find its
* ordinary copy's header, and keeps its third copy out of the loop. */
/** Iterations between cancellation polls of a strip-mined counted loop. */
const WORKER_POLL_BLOCK = 64;
/** The longest run of a strip-mined loop inside another loop that polls
* only as it leaves. */
const WORKER_UNPOLLED_RUN = 16384;
/** Whether a step of `step` from every value in `range` gives a different
* value. Beyond 2^53 a double can stop changing (`j + 1 === j` from 2^53),
* so a counter there can run without end inside a range of a few values. */
function exactSteps(range: { min: number; max: number }, step: number): boolean {
const size = Math.abs(step);
return range.min >= size - 2 ** 53 && range.max <= 2 ** 53 - size;
}
/** At most how many times a `for` loop and the loops in its body run their
* bodies per entry, from the counters' proven ranges: its iterations times
* one plus the nested loops' runs. Infinity when a count has no bound, or
* depends on a run-time guard (the guard's slow copy steps a double). */
function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number {
if (loop.kind !== "for") return Infinity;
const counted = matchIntegerCountedForLoop(
loop,
emitter.numericLocals,
emitter.integerRanges,
LOOPS.of(emitter).unaliasedArrays,
);
let trips = Infinity;
if (counted && !counted.guarded && exactSteps(counted.range, counted.step))
trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1;
else if (!counted) {
// Each iteration closes the gap between the two counters by at least two.
const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges);
if (paired && !paired.guarded && exactSteps(paired.range, 1))
trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1;
}
if (trips === Infinity) {
// A counter with a proven range where the condition reads it, such as
// one compared with a module constant.
const stepped = steppedLoopCounter(loop, emitter.currentLocals);
const range = stepped && emitter.integerRanges.get(stepped.ref);
if (stepped && range && exactSteps(range, stepped.step))
trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1;
}
if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity;
let nested = 0;
const visit = (s: IrStmt): boolean => {
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") {
nested += boundedLoopRuns(emitter, s);
return nested !== Infinity;
}
return everyStmtChild(s, () => true, visit);
};
if (!loop.body.every(visit)) return Infinity;
return trips * (1 + nested);
}
/** The counter of a strip-mined loop: its 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.
*
* With `runSlot` (for a loop that another loop of the function encloses), a
* block that ends at the limit leaves the loop without polling, and a run
* of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as
* it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot`
* holds whether the current block is such a run. */
function emitStripMinedCountedLoop(
emitter: LlEmitter,
s: Extract<IrStmt, { kind: "for" }>,
counter: StripMinedCounter,
boundSlot: string,
exit: string,
runSlot: string | null,
): void {
const B = emitter.B;
const { slot, type, signed, step } = counter;
const ascending = step > 0;
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
const wrap = signed ? "nsw" : "nuw";
const lc = B.newLabel("loop.c");
const block = B.newLabel("loop.block");
const bc = B.newLabel("loop.bc");
const lb = B.newLabel("loop.b");
const lu = B.newLabel("loop.u");
const poll = B.newLabel("loop.poll");
emitter.declare(`declare ${type} @${saturating}(${type}, ${type})`);
emitter.declare(`declare ${type} @${nearer}(${type}, ${type})`);
B.br(lc);
B.startBlock(lc);
enterLoopHeader(emitter, lc);
const start = B.tmp();
const open = B.tmp();
B.line(`${start} = load ${type}, ptr ${slot}`);
const limit = counter.limit();
B.line(`${open} = icmp ${compare} ${type} ${start}, ${limit}`);
B.condBr(open, block, exit);
B.startBlock(block);
const stepped = B.tmp();
const bound = B.tmp();
B.line(
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
);
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
else {
// The steps left to the limit: positive, as the header tested.
const left = B.tmp(),
whole = B.tmp(),
long = B.tmp(),
polled = B.tmp(),
end = B.tmp();
const stride = Math.abs(step);
B.line(
ascending
? `${left} = sub ${wrap} ${type} ${limit}, ${start}`
: `${left} = sub ${wrap} ${type} ${start}, ${limit}`,
);
B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`);
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`);
B.line(`${polled} = and i1 ${whole}, ${long}`);
B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`);
B.line(`store ${type} ${end}, ptr ${boundSlot}`);
B.line(`store i1 ${polled}, ptr ${runSlot}`);
}
B.br(bc);
B.startBlock(bc);
const index = B.tmp();
const end = B.tmp();
const inBlock = B.tmp();
B.line(`${index} = load ${type}, ptr ${slot}`);
B.line(`${end} = load ${type}, ptr ${boundSlot}`);
B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`);
const after = runSlot === null ? poll : B.newLabel("loop.end");
B.condBr(inBlock, lb, after);
B.startBlock(lb);
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);
if (runSlot !== null) {
// A speculative copy may enter the body without passing the header, so
// the limit is read again here.
B.startBlock(after);
const at = B.tmp();
const more = B.tmp();
const run = B.tmp();
const done = B.newLabel("loop.done");
const last = B.newLabel("loop.poll.run");
B.line(`${at} = load ${type}, ptr ${slot}`);
B.line(`${more} = icmp ${compare} ${type} ${at}, ${counter.limit()}`);
B.condBr(more, poll, done);
B.startBlock(done);
B.line(`${run} = load i1, ptr ${runSlot}`);
B.condBr(run, last, exit);
B.startBlock(last);
emitter.emitWorkerPoll();
B.br(exit);
}
B.startBlock(poll);
emitter.emitWorkerPoll();
B.br(lc);
}
/** Whether `body` holds a loop outside its closures. */
function containsLoop(body: IrStmt[]): boolean {
const inStmt = (s: IrStmt): boolean =>
s.kind === "while" ||
s.kind === "doWhile" ||
s.kind === "for" ||
s.kind === "forOf" ||
!everyStmtChild(
s,
() => true,
(child) => !inStmt(child),
);
return body.some(inStmt);
}
function enterLoopHeader(emitter: LlEmitter, header: string): void {
const dispatch = emitter.speculationHeader;
emitter.speculationHeader = null;
@@ -2531,9 +2970,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 +3000,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");
@@ -211,6 +211,19 @@ test("a target CPU level reaches every function attribute group and nothing else
expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain);
});
test("only sanitized builds mark the emitted functions sanitize_address", () => {
const module = moduleFor([call()]);
const sanitized = emitLlvmModule(module);
expect(emitLlvmModule(module, { sanitizeAttributes: true })).toBe(sanitized);
expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address \}$/m);
const plain = emitLlvmModule(module, { sanitizeAttributes: false });
expect(plain).not.toContain("sanitize_address");
// A group needs an attribute: an ignored string attribute stands in.
expect(plain).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m);
const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" });
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
});
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
for (const boxed of [false, true]) {
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ import { alwaysExactInteger, exactInteger } from "./integer-values.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { emitByteNumber } from "./byte-numbers.js";
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
import { bytesElementAlias, bytesHeaderAlias, bytesHeaderValueAlias } from "./bytes-alias.js";
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
u8: { kind: 0, le: false },
@@ -160,12 +160,19 @@ export function emitBytesIndex(
return idx;
}
/** The element count of a typed array, in the target's size type. */
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
/** The element count of a typed array, in the target's size type. `value`:
* the program reads the count itself (bytesHeaderValueAlias), rather than
* only bounding an access. */
export function emitBytesHeaderLength(
host: LlvmEmitterContext,
receiver: string,
value = false,
): string {
const p = host.B.tmp();
const len = host.B.tmp();
const alias = value ? bytesHeaderValueAlias(host) : bytesHeaderAlias(host);
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${alias}`);
return len;
}
@@ -184,7 +191,7 @@ export function emitBytesLength(
bytes: boolean,
): LlValue {
const B = host.B;
const len = emitBytesHeaderLength(host, receiver);
const len = emitBytesHeaderLength(host, receiver, true);
const size = BYTES_ELEMENT_SIZE[elem];
const count = bytes && size !== 1 ? B.tmp() : len;
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
@@ -235,6 +242,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 +574,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 +816,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 };
}
@@ -1,3 +1,4 @@
import { dynDesc } from "../../ir/analysis.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
import { InternalCompilerError } from "../../errors.js";
@@ -42,17 +43,21 @@ import {
emitStringMapGetRef,
emitStringMapGetScalar,
emitStringMapHas,
noteNumberMapKey,
} from "./map-number-lookup.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
import {
STRING_CHAR_AT_DECL,
STRING_CP_AT_BYTE_DECL,
STRING_CHAR_AT_INT_MARKER,
STRING_CHAR_CODE_AT_DECL,
STRING_CHAR_CODE_AT_INT_MARKER,
emitAsciiChar,
emitAsciiCharCode,
emitStringByteLength,
emitUnitCharCode,
STRING_UTF16_LEN_DECL,
} from "./string-reads.js";
import { exactInteger, widenInteger } from "./integer-values.js";
@@ -322,6 +327,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 +378,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 +395,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);
@@ -546,16 +567,30 @@ export function emitStrIntrinsic(
return call("scr_str_is_well_formed", "zeroext i1 (ptr)", `ptr ${r.name}`, "i1", false);
case "toWellFormed":
return call("scr_str_to_well_formed", "ptr (ptr)", `ptr ${r.name}`, "ptr", true);
case "cpAt":
// The code point AT an index as a one-code-point string (+1) —
// the string-for-of desugar's read.
return call(
"scr_str_cp_at",
"ptr (ptr, double)",
`ptr ${r.name}, double ${args[0]!.name}`,
"ptr",
true,
);
case "byteLength": {
const t = B.tmp();
// A byte count fits in a signed size, whose conversion is one instruction.
B.line(`${t} = sitofp ${host.sizeType} ${emitStringByteLength(host, r.name)} to double`);
return { name: t, type: e.type };
}
case "cpAtByte": {
// sc_str_cp_at_byte (string-reads.ts) answers ASCII bytes from the
// charAt table inline. The cursor is a character start, an exact
// integer below the byte count.
host.declare(STRING_CHAR_AT_DECL);
host.declare(STRING_CP_AT_BYTE_DECL);
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
let wide: string;
if (index) {
wide = widenInteger(host, index);
} else {
wide = B.tmp();
B.line(`${wide} = fptosi double ${args[0]!.name} to i64`);
}
const t = B.tmp();
B.line(`${t} = call ptr @sc_str_cp_at_byte(ptr ${r.name}, i64 ${wide})`);
return host.own({ name: t, type: e.type });
}
default: {
const _exhaustive: never = method;
void _exhaustive;
@@ -772,6 +807,9 @@ export function emitArrIntrinsic(
case "shift": {
host.emitPublishedArrayGuard(r.name, e.receiver.type, method === "pop" ? 2 : 6);
const checkedValue = elem.kind === "dyn" && e.type.kind === "dyn";
// A foreign element (SCR_ARR_FOREIGN) cannot be the typed result:
// removing one throws the checked conversion's TypeError.
const foreignElements = !checkedValue && host.mayHoldForeign(elem);
if (!checkedValue && e.type.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: array removal result is not a union");
const def = e.type.kind === "union" ? host.unionsById.get(e.type.unionId) : undefined;
@@ -814,7 +852,25 @@ export function emitArrIntrinsic(
);
B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
B.line(`${has} = icmp eq i8 ${state}, 1`);
B.condBr(has, lp, la);
const lf = foreignElements ? B.newLabel("remove.f") : la;
B.condBr(has, lp, lf);
if (foreignElements) {
// Only the runtime call removes a foreign element; its value moves
// out with the slot, and the throw releases it.
B.startBlock(lf);
const foreign = B.tmp();
const throwLabel = B.newLabel("remove.foreign");
B.line(`${foreign} = icmp eq i8 ${state}, 3`);
B.condBr(foreign, throwLabel, la);
B.startBlock(throwLabel);
const payload = B.tmp();
B.line(`${payload} = inttoptr i64 ${slowRaw} to ptr`);
host.declare(`declare void @scr_dyn_foreign_throw(ptr, ptr)`);
const want = host.cstr(dynDesc(e.type, host.recordsById, host.unionsById));
B.line(`call void @scr_dyn_foreign_throw(ptr ${payload}, ptr ${want})`);
host.emitAlwaysPendingUnwind();
B.br(la);
}
B.startBlock(lp);
let raw = slowRaw;
const value = B.tmp();
@@ -1127,6 +1183,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 +1317,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).
@@ -1529,6 +1646,7 @@ 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);
@@ -1555,6 +1673,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)),
@@ -1564,7 +1683,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 }>;
@@ -2,11 +2,14 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, isUnitType, typeEquals, type IrExpr } from "../../ir/ir.js";
import { isRefCounted, isUnitType, typeEquals, type IrExpr, type IrType } from "../../ir/ir.js";
import { DYN_KIND } from "./dyn.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { emitRecordKeyGetOr } from "./expr-records.js";
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
import { emitUnionTag } from "./union-repr.js";
export function emitControlExpr(
host: LlvmEmitterContext,
@@ -52,6 +55,8 @@ export function emitControlExpr(
case "toBool":
return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type };
case "logical": {
const tagSet = e.op === "||" ? unionTagSet(e) : null;
if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type };
// JS value semantics: the result is the deciding operand itself.
// Left evaluates once, ownership moves into the result slot; when
// the branch takes the right operand the stale left releases first
@@ -393,6 +398,13 @@ export function emitControlExpr(
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
const fused = emitNullishOptionalChain(host, e);
if (fused) return fused;
if (
e.left.kind === "recordKeyGet" &&
e.left.missNaN !== true &&
typeEquals(e.type, e.left.type) &&
storesNoUnit(host, e.left)
)
return emitRecordKeyGetOr(host, e.left, e.right);
if (
!typeEquals(e.type, e.left.type) &&
(e.left.kind === "varRef" ||
@@ -614,3 +626,97 @@ function literalChoice(e: IrExpr): boolean {
if (e.kind === "strLit") return true;
return e.kind === "ternary" && literalChoice(e.then) && literalChoice(e.else_);
}
type TagSet = { unionId: string; value: IrExpr; tags: Set<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;
}
/** Every value a keyed read can hit is present (no stored undefined or
* null), so only a missing key yields the read's unit arm. */
function storesNoUnit(host: LlvmEmitterContext, e: ExprOf<"recordKeyGet">): boolean {
const shape = host.recordShape(e.shapeId);
const holdsUnit = (t: IrType): boolean =>
isUnitType(t) ||
t.kind === "dyn" ||
(t.kind === "union" && (host.unionsById.get(t.unionId)?.arms ?? []).some(isUnitType));
if (shape.indexValue !== undefined && holdsUnit(shape.indexValue)) return false;
return e.overflowOnly === true || !shape.fields.some((f) => holdsUnit(f.type));
}
@@ -31,7 +31,8 @@ import { elemAccess, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { emitUnionWiden } from "./expr-records.js";
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
import { jsIcLiteralKey } from "../../ir/js-ic.js";
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
export function emitDynamicExpr(
@@ -89,7 +90,7 @@ export function emitDynamicExpr(
// Bytes already box their shared mutable storage directly. A second
// capsule would split identity between checked and native views.
const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className);
// A TextEncoder or TextDecoder record boxes as its own handle, which
// A TextEncoder, TextDecoder or StringDecoder record boxes as its own handle, which
// keeps the record's identity (toDynHelper).
const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null;
if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
@@ -121,6 +122,10 @@ export function emitDynamicExpr(
const box = host.dyn.dynFuncBoxHelper(v.type);
const t = B.tmp();
B.line(`${t} = call ptr @${box}(ptr ${v.name}, ptr ${name})`);
if (e.fnLength !== undefined) {
host.declare(`declare void @scr_dyn_function_set_length(ptr, i32)`);
B.line(`call void @scr_dyn_function_set_length(ptr ${t}, i32 ${e.fnLength})`);
}
return host.own({ name: t, type: e.type });
}
const conv = host.dyn.toDynHelper(v.type);
@@ -312,9 +317,14 @@ export function emitDynamicExpr(
const obj = B.tmp();
B.line(`${obj} = call ptr @scr_dyn_new_obj()`);
const out = host.own({ name: obj, type: e.type });
const cached = jsIcMethods(host);
for (const f of e.fields ?? []) {
const k = emitBorrowedInput(host, f.key);
const v = emitBorrowedInput(host, f.value);
if (cached && jsIcLiteralKey(f.key) !== null) {
emitJsIcLiteralMember(host, obj, k.name, v.name);
continue;
}
B.line(
`call void @${f.key.type.kind === "dyn" ? "scr_dyn_key_set_computed" : "scr_dyn_key_set"}(ptr ${obj}, ptr ${k.name}, ptr ${v.name})`,
);
@@ -13,13 +13,14 @@ import { emitStringInputs } from "./string-lifetimes.js";
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
import {
alwaysExactInteger,
exactCompareOperands,
exactInteger,
int32Number,
integerNumber,
narrowInteger,
wideArithmetic,
widenInteger,
type LlInteger,
} from "./integer-values.js";
import {
bitwiseIdentityOperand,
@@ -45,7 +46,7 @@ import {
slotWidth,
type ArrayAccess,
} from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
import { emitWrappingSum, integerOrNaNRange, markWrappingOperands } from "./wrapping-sums.js";
import { emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
/** The signed integer predicates of the number compares. */
@@ -58,6 +59,63 @@ const INTEGER_COMPARES: Record<string, string> = {
"!==": "ne",
};
const UNSIGNED_COMPARES: Record<string, string> = {
"<": "ult",
"<=": "ule",
">": "ugt",
">=": "uge",
"===": "eq",
"!==": "ne",
};
/** A number compare of operands that are each an exact integer or an
* integer-or-NaN value (integerOrNaNRange: integer typed-array elements,
* `charCodeAt` results, sums of those), all within int32 or all within
* uint32: their 32-bit views compared, and NaN comparing false (unequal for
* `!==`). The 32-bit view is the value's ToUint32, which holds its bits
* whenever it is not NaN. Null when an operand has no such view. */
function emitIntegerOrNaNCompare(
host: LlvmEmitterContext,
e: ExprOf<"bin">,
l: LlValue,
li: LlInteger | null,
r: LlValue,
ri: LlInteger | null,
): string | null {
const lr = li?.range ?? integerOrNaNRange(host, e.left),
rr = ri?.range ?? integerOrNaNRange(host, e.right);
if (!lr || !rr) return null;
const signed = withinInt32(lr) && withinInt32(rr);
if (!signed && !(withinUint32(lr) && withinUint32(rr))) return null;
const bits = (value: LlValue, integer: LlInteger | null): string | undefined =>
integer ? narrowInteger(host, integer) : value.uint32;
const left = bits(l, li),
right = bits(r, ri);
if (left === undefined || right === undefined) return null;
const B = host.B;
const compare = B.tmp();
B.line(
`${compare} = icmp ${(signed ? INTEGER_COMPARES : UNSIGNED_COMPARES)[e.op]} i32 ${left}, ${right}`,
);
let result = compare;
for (const [value, integer] of [
[l, li],
[r, ri],
] as const) {
if (integer) continue;
const ordered = B.tmp(),
combined = B.tmp();
B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`);
if (e.op === "!==") {
const unordered = B.tmp();
B.line(`${unordered} = xor i1 ${ordered}, true`);
B.line(`${combined} = or i1 ${result}, ${unordered}`);
} else B.line(`${combined} = and i1 ${result}, ${ordered}`);
result = combined;
}
return result;
}
export function emitLiteralExpr(
host: LlvmEmitterContext,
e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">,
@@ -363,14 +421,16 @@ export function emitOperatorExpr(
return value;
}
} else {
// An operand with only a wide view, such as `i + 1` of a speculated
// counter: compare the integers when both always equal the doubles.
const left = alwaysExactInteger(host, l, li),
right = left === null ? null : alwaysExactInteger(host, r, ri);
if (left !== null && right !== null) {
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
// An operand with only a wide view, such as `i + 1` or `2 * i + 1`
// of a speculated counter: compare the integers when that gives the
// doubles' result.
const operands = exactCompareOperands(host, l, li, r, ri);
if (operands) {
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${operands[0]}, ${operands[1]}`);
return { name: t, type: e.type };
}
const nanCoded = emitIntegerOrNaNCompare(host, e, l, li, r, ri);
if (nanCoded) return { name: nanCoded, type: e.type };
B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
}
} else if (bit[e.op] !== undefined) {
@@ -601,6 +661,29 @@ export function regexLiteralTemplate(
return `@${re.sym}`;
}
/** The build-time replacer of a global substitution of regex literal `e`
* with the literal `replacement` (regex-matchers.ts), registered on first
* use. */
export function regexLiteralReplacer(
host: LlvmEmitterContext,
e: IrExpr & { kind: "regexLit" },
replacement: string,
): string {
const key = `${e.flags}/${e.pattern}\0${replacement}`;
let rr = host.regexReplacers.get(key);
if (!rr) {
rr = {
sym: `sc_rr_${host.regexReplacers.size}`,
pattern: e.pattern,
flags: e.flags,
replacement,
};
host.regexReplacers.set(key, rr);
}
host.journal?.entry("regexReplacer", key, rr);
return `@${rr.sym}`;
}
/** A string equality or comparison between `s.charAt(i)` and a literal of
* exactly one UTF-16 code unit: the charAt call, the unit, and whether the
* charAt is the left operand. */
@@ -886,6 +969,34 @@ export function emitStringExpr(
}
}
/** scr_arr_reserve_len reserves dense storage only up to this many slots
* (SCR_ARR_DENSE_LIMIT, scr_array.c). */
const RESERVE_LIMIT = 2 ** 20;
/** Elements the inline slots of a small array construction
* (scr_arr_new_small, scr_arr_new_ops_small) hold at least
* (SCR_ARR_SMALL_SLOTS, scr_array.c). */
const SMALL_ARRAY_SLOTS = 3;
/** The capacity scr_arr_reserve_len would reserve for `length` on an empty
* array: the length truncated when it is at least 1 and at most
* RESERVE_LIMIT, else 0. */
function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string {
const B = host.B;
const n = host.emitExpr(length);
const atLeastOne = B.tmp(),
withinLimit = B.tmp(),
reserves = B.tmp(),
slots = B.tmp(),
capacity = B.tmp();
B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`);
B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`);
B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`);
B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`);
B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`);
return capacity;
}
export function emitContainerExpr(
host: LlvmEmitterContext,
e: ExprOf<
@@ -957,7 +1068,34 @@ export function emitContainerExpr(
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
);
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
} else {
const hint = host.reservedArrayLengths.get(e);
if (hint !== undefined) {
host.reservedArrayLengths.delete(e);
host.consumedReserves.add(hint);
const capacity = reservedCapacity(host, hint.length);
// The small construction's inline slots hold at least
// SMALL_ARRAY_SLOTS elements; larger capacities are sized.
const fits = B.tmp(),
smallArr = B.tmp(),
sizedArr = B.tmp();
const smallLabel = B.newLabel("arr.cap.small"),
sizedLabel = B.newLabel("arr.cap.sized"),
joined = B.newLabel("arr.cap.j");
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
B.condBr(fits, smallLabel, sizedLabel);
B.startBlock(smallLabel);
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
B.br(joined);
B.startBlock(sizedLabel);
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
B.br(joined);
B.startBlock(joined);
B.line(
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
);
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
}
const out = host.own({ name: arr, type: e.type });
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
let pending: LlValue[] = [];
@@ -6,7 +6,8 @@ import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { f64Lit, llvmCommentText } from "./common.js";
import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
import { regexLiteralTemplate } from "./expr-primitives.js";
import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js";
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
import { preservesRegexInputs } from "./reference-effects.js";
@@ -160,6 +161,39 @@ function directRegexOperandMethod(method: string): boolean {
}
}
/** The build-time replacer of a substitution: a regex literal running on
* its template, with a literal replacement the replacer can write as is.
* Null when the substitution takes the runtime's loop. */
function literalReplacer(
host: LlvmEmitterContext,
literal: IrExpr | null,
template: string | null,
replacement: IrExpr,
): string | null {
if (literal === null || template === null || literal.kind !== "regexLit") return null;
if (replacement.kind !== "strLit") return null;
if (!canReplaceWith(literal.pattern, literal.flags, replacement.value)) return null;
if (!regexLiteralCompiles(literal.pattern, literal.flags)) return null;
return regexLiteralReplacer(host, literal, replacement.value);
}
/** scr_regex_replace_with: the runtime runs `replacer` on an ASCII subject
* and its own loop on any other. */
function emitReplaceWith(
host: LlvmEmitterContext,
e: IrExpr,
r: LlValue,
args: LlValue[],
replacer: string,
): LlValue {
host.declare(`declare ptr @scr_regex_replace_with(ptr, ptr, ptr, ptr)`);
const t = host.B.tmp();
host.B.line(
`${t} = call ptr @scr_regex_replace_with(ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${replacer})`,
);
return host.own({ name: t, type: e.type });
}
export function emitRegexIntrinsic(
host: LlvmEmitterContext,
e: IrExpr & { kind: "regexIntrinsic" },
@@ -234,11 +268,16 @@ export function emitRegexIntrinsic(
"scr_regex_match_all_captures_into",
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
);
case "replaceAll":
case "replaceAll": {
// A global literal (the only kind a replacer is built for) never takes
// replaceAll's non-global TypeError.
const replacer = literalReplacer(host, literal, template, e.args[1]!);
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
return fallible(
"scr_regex_replace_all",
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
);
}
case "split":
host.declare(`declare ptr @scr_regex_split_limit(ptr, ptr, double)`);
{
@@ -323,6 +362,8 @@ export function emitRegexIntrinsic(
return host.own({ name: t, type: e.type });
}
case "replace": {
const replacer = literalReplacer(host, literal, template, e.args[1]!);
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
host.declare(`declare ptr @scr_regex_replace(ptr, ptr, ptr)`);
const t = B.tmp();
B.line(
@@ -338,6 +379,39 @@ export function emitRegexIntrinsic(
}
}
/** `r[k] ?? d` in its pass-through shape: a missing key evaluates `d`
* where the read would store undefined, so a hit costs nothing extra. */
export function emitRecordKeyGetOr(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordKeyGet" },
fallback: IrExpr,
): LlValue {
const B = host.B;
const obj = host.emitExpr(e.obj);
const k = host.emitExpr(e.key);
const ty = host.llType(e.type);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty}`);
const join = B.newLabel("rkg.j");
keyedRecordReadInto(
host,
slot,
join,
obj.name,
k.name,
e.shapeId,
e.type,
e.overflowOnly === true,
e.loc,
false,
() => host.emitBranchInto(slot, fallback),
);
B.startBlock(join);
const t = B.tmp();
B.line(`${t} = load ${ty}, ptr ${slot}`);
return host.own({ name: t, type: e.type });
}
export function emitRecordKeyGet(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordKeyGet" },
@@ -485,6 +559,7 @@ export function keyedRecordReadInto(
overflowOnly: boolean,
loc?: SrcLoc,
missNaN = false,
missInto: (() => void) | null = null,
): void {
const B = host.B;
const shape = host.recordShape(shapeId);
@@ -687,8 +762,10 @@ export function keyedRecordReadInto(
}
}
if (def !== null && resultType.kind === "union" && undefTag >= 0) {
// The miss path: the result union's undefined arm.
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
// The miss path: the result union's undefined arm, or the caller's
// own value for a miss (missInto stores it into the slot).
if (missInto !== null) missInto();
else B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
B.br(join);
return;
}
@@ -1,7 +1,7 @@
import { typedRefConstructor } from "./shapes.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
import { streamRooted, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
import {
type IrRecordShape,
type IrType,
@@ -184,7 +184,11 @@ export function streamTypedRefCommitAdapter(
}
if (t.kind === "array") {
const commit = `${snapshot}_commit`;
const check = host.dyn.dynCheckHelper(t);
// A value the element type cannot hold stays in the array as a foreign
// element where the program can store one (LlEmitter.mayHoldForeign).
const check = host.mayHoldForeign(t.elem)
? host.dyn.dynViewArrayHelper(t)
: host.dyn.dynCheckHelper(t);
host.declare(`declare void @scr_arr_release(ptr)`);
host.declare(`declare void @scr_arr_outline(ptr)`);
host.declare(`declare void @scr_arr_adopt_view_properties(ptr, ptr, ptr)`);
@@ -438,10 +442,14 @@ export function streamTypedRefCommitAdapter(
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
host.declare(`declare void @scr_str_release(ptr)`);
host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`);
// The view of a sealed or frozen instance holds its fields as
// non-configurable entries, which the bag copies.
const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES);
const placeholderSet = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set";
if (meta.def.tracksOwnFields) {
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`);
}
host.declare(`declare void @${placeholderSet}(ptr, ptr, ${host.sizeType}, ptr)`);
} else if (withAttributes) host.declare(`declare void @scr_dyn_obj_delete_field(ptr, ptr)`);
lines.push(
` %bag = call ptr @scr_dyn_new_obj()`,
` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`,
@@ -478,14 +486,16 @@ export function streamTypedRefCommitAdapter(
`bag_field${index}_present:`,
` %bag_undefined${index} = call ptr @scr_dyn_undefined()`,
` %bag_placeholder${index} = call ptr @scr_dyn_retain_v(ptr %bag_undefined${index})`,
` call void @scr_dyn_obj_set(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`,
` call void @${placeholderSet}(ptr %bag, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr %bag_placeholder${index})`,
` br label %bag_field${index}_after`,
`bag_field${index}_after:`,
);
else
lines.push(
` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`,
withAttributes
? ` call void @scr_dyn_obj_delete_field(ptr %bag, ptr %bag_key${index})`
: ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`,
` call void @scr_str_release(ptr %bag_key${index})`,
);
});
@@ -493,7 +503,9 @@ export function streamTypedRefCommitAdapter(
host.declare(
meta.def.tracksOwnFields
? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)`
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
: withAttributes
? `declare void @scr_dyn_obj_delete_symbol_field(ptr, ptr)`
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
);
[...symbols.values()].forEach((symbol, index) => {
const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`);
@@ -503,7 +515,9 @@ export function streamTypedRefCommitAdapter(
...keyLines.release,
meta.def.tracksOwnFields
? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})`
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
: withAttributes
? ` call void @scr_dyn_obj_delete_symbol_field(ptr %bag, ptr %bag_sym${index})`
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
);
});
@@ -519,6 +533,20 @@ export function streamTypedRefCommitAdapter(
host.declare(`declare void @scr_error_commit_dyn(ptr, ptr)`);
lines.push(` call void @scr_error_commit_dyn(ptr %target, ptr %d)`);
}
// Untyped Object.freeze, seal and preventExtensions apply to the view;
// the instance's integrity word records them.
if (host.untypedIntegrity && meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES)) {
const bag = meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)
? "%bag"
: "null";
host.declare(`declare double @scr_dyn_class_view_integrity(ptr, double, ptr)`);
lines.push(
` %word_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`,
` %word = load double, ptr %word_ptr`,
` %word_next = call double @scr_dyn_class_view_integrity(ptr %d, double %word, ptr ${bag})`,
` store double %word_next, ptr %word_ptr`,
);
}
lines.push(` br label %done`, `done:`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...lines);
return `@${commit}`;
@@ -768,13 +796,16 @@ export function streamTypedRefMaterializeAdapter(
t: IrType,
): LlStreamTypedRefAdapter {
const key = typeKey(t);
const existing = host.liveDynRefAdapters.get(key);
// A `(T | undefined)[]` stored as T elements shares T[]'s capsule type,
// but its adapters also store undefined elements.
const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key;
const existing = host.liveDynRefAdapters.get(adapterKey);
if (existing) return existing;
// The converter depends only on the static type. Intern the complete
// graph across roots and union arms so recursive types emit once.
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
host.liveDynRefAdapters.set(key, adapter);
host.liveDynRefAdapters.set(adapterKey, adapter);
host.journal?.allocated(
snapshot.replace(/_materialize$/, ""),
"streamTypedRefMaterializeAdapter",
@@ -843,8 +874,7 @@ export function streamTypedRefMaterializeAdapter(
host.declare(`declare ptr @scr_error_dyn_fields(ptr)`);
B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`);
} else if (
meta.def.runtime &&
RUNTIME_STREAM_CLASSES.has(t.className) &&
((meta.def.runtime && RUNTIME_STREAM_CLASSES.has(t.className)) || streamRooted(meta)) &&
!meta.def.instancePrototypeHelper &&
!meta.def.prototypeDataHelper
) {
@@ -980,9 +1010,15 @@ export function streamTypedRefMaterializeAdapter(
host.declare(`declare void @scr_union_release(ptr)`);
B.line(`call void @scr_union_release(ptr ${fieldValue})`);
}
host.declare(`declare void @scr_dyn_symbol_key_set(ptr, ptr, ptr)`);
// As for string-keyed fields, attributes copied from the bag (a frozen
// instance's) do not stop the view taking the value.
const symbolSetter =
meta.def.tracksOwnFields || withAttributes
? "scr_dyn_symbol_field_set"
: "scr_dyn_symbol_key_set";
host.declare(`declare void @${symbolSetter}(ptr, ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
B.line(`call void @scr_dyn_symbol_key_set(ptr ${out}, ptr ${key}, ptr ${value})`);
B.line(`call void @${symbolSetter}(ptr ${out}, ptr ${key}, ptr ${value})`);
B.line(`call void @scr_dyn_release_v(ptr ${value})`);
if (afterSymbol) {
B.terminate(`br label %${afterSymbol}`);
@@ -1203,6 +1239,21 @@ export function streamTypedRefMaterializeAdapter(
B.line(`call void @scr_dyn_arr_push_hole(ptr ${out})`);
B.br(doneLabel);
B.startBlock(undefinedLabel);
if (host.mayHoldForeign(elem)) {
// A foreign element (SCR_ARR_FOREIGN) holds its own value.
const foreign = B.tmp();
const foreignLabel = B.newLabel("live.array.foreign");
const presentUndefined = B.newLabel("live.array.present.undefined");
B.line(`${foreign} = fcmp oeq double ${state}, 3.0`);
B.condBr(foreign, foreignLabel, presentUndefined);
B.startBlock(foreignLabel);
host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`);
const payload = B.tmp();
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %p, double ${index}) ; +1`);
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${payload})`);
B.br(doneLabel);
B.startBlock(presentUndefined);
}
const undefinedValue = B.tmp();
const retained = B.tmp();
B.line(`${undefinedValue} = call ptr @scr_dyn_undefined()`);
@@ -1283,13 +1334,34 @@ function liveArrayOps(
G.terminate(`ret ptr ${boxed}`);
const S = new BlockBuilder();
if (t.optionalElements) {
// A `(T | undefined)[]` stores undefined as a present undefined.
const undef = host.dyn.isUndefined(S, "%v");
const storeUndefined = S.newLabel("live.array.undefined");
const other = S.newLabel("live.array.value");
S.condBr(undef, storeUndefined, other);
S.startBlock(storeUndefined);
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`);
S.terminate(`ret i1 true`);
S.startBlock(other);
}
const fits = S.tmp();
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
const store = S.newLabel("live.array.store");
const refuse = S.newLabel("live.array.refuse");
S.condBr(fits, store, refuse);
S.startBlock(refuse);
S.terminate(`ret i1 false`);
if (host.mayHoldForeign(elem)) {
// The array keeps a value its element type cannot hold as a foreign
// element, and undefined as a present undefined.
host.declare(`declare zeroext i1 @scr_arr_set_foreign(ptr, double, ptr)`);
const kept = S.tmp();
S.line(`${kept} = call zeroext i1 @scr_arr_set_foreign(ptr %p, double %i, ptr %v)`);
S.terminate(`ret i1 ${kept}`);
} else {
S.terminate(`ret i1 false`);
}
S.startBlock(store);
const next = S.tmp();
S.line(
@@ -0,0 +1,45 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ForkPrimitives } from "./emitter.js";
/** Starts `compute` on a forked child process, which inherits this process
* as it is at the fork, and returns the join that answers its result. The
* child writes `encode`'s text to a file and exits; the join waits for it
* and decodes the text. When the fork fails, the child fails, or `encode`
* answers null, the join runs `compute` here instead, so the result does
* not depend on the child. Join once. */
export function forkedAnalysis<T>(
fork: ForkPrimitives,
compute: () => T,
encode: (value: T) => string | null,
decode: (text: string) => T,
): () => T {
const dir = mkdtempSync(join(tmpdir(), "scriptc-analysis-"));
const file = join(dir, "result.json");
const pid = fork.fork();
if (pid === 0) {
let code = 1;
try {
const text = encode(compute());
if (text !== null) {
writeFileSync(file, text);
code = 0;
}
} catch {
// The parent computes the result itself.
}
fork.exit(code);
}
return () => {
let text: string | null = null;
try {
if (pid > 0 && fork.wait(pid) === 0) text = readFileSync(file, "utf8");
} catch {
// The result is computed here.
} finally {
rmSync(dir, { recursive: true, force: true });
}
return text === null ? compute() : decode(text);
};
}
@@ -111,13 +111,17 @@ export function wideArithmetic(
rv = view(r, ri);
if (!lv || !rv) return null;
let below: number, bound: number;
let single = false;
if (op === "*") {
if (lv.below !== Infinity || rv.below !== Infinity) return null;
below = EXACT_DOUBLES;
bound = productBound(lv.bound, rv.bound);
// Scaling by a power of two never rounds a double that stays finite.
below = powerOfTwo(li) || powerOfTwo(ri) ? Infinity : EXACT_DOUBLES;
single = true;
} else if (lv.below === Infinity && rv.below === Infinity) {
below = EXACT_DOUBLES;
bound = sumBound(lv.bound, rv.bound);
single = true;
} else if (lv.below === Infinity || rv.below === Infinity) {
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
@@ -132,7 +136,50 @@ export function wideArithmetic(
host.B.line(
`${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
);
return { name, below, bound };
return single ? { name, below, bound, single } : { name, below, bound };
}
/** Whether an exact integer is the constant 2^k or -2^k. */
function powerOfTwo(integer: LlInteger | null): boolean {
if (integer === null || integer.range.min !== integer.range.max) return false;
const value = Math.abs(integer.range.min);
// Math.log2 can round to an integer for a value that is not a power of
// two (2^52 - 1), so the candidate power is compared with the value.
return value >= 1 && value <= 2 ** 52 && 2 ** Math.round(Math.log2(value)) === value;
}
/** The i64 operands of a number compare that gives the doubles' result:
* both equal their doubles wherever they can be, or one is an exact integer
* of magnitude at most 2^53 - 1 and the other's double is its integer
* rounded once. Rounding is monotonic, and b - 1, b and b + 1 are doubles
* for a safe integer b, so an integer rounded once compares with b as the
* integer does. Null for any other pair. */
export function exactCompareOperands(
host: LlvmEmitterContext,
l: LlValue,
li: LlInteger | null,
r: LlValue,
ri: LlInteger | null,
): [string, string] | null {
const describe = (value: LlValue, integer: LlInteger | null) => {
if (integer) return { exact: true, single: true, bound: magnitude(integer.range) };
const wide = value.wideInteger;
if (!wide) return null;
return { exact: wide.bound <= wide.below, single: wide.single === true, bound: wide.bound };
};
const a = describe(l, li),
b = describe(r, ri);
if (!a || !b) return null;
const safe = (d: { exact: boolean; bound: number }): boolean =>
d.exact && d.bound <= Number.MAX_SAFE_INTEGER;
const ok =
(a.exact && b.exact) ||
(safe(a) && (b.exact || b.single)) ||
(safe(b) && (a.exact || a.single));
if (!ok) return null;
const operand = (value: LlValue, integer: LlInteger | null): string =>
integer ? widenInteger(host, integer) : value.wideInteger!.name;
return [operand(l, li), operand(r, ri)];
}
/** An i64 index for a consumer that compares it, unsigned, with a length or
+149 -14
View File
@@ -6,13 +6,16 @@
*
* load ordinary object whose shape is entry 0's own-property shape:
* read entry `slot` inline unless it is an accessor; otherwise
* scr_js_load_probe (polymorphic, prototype hits, misses)
* scr_js_get_probe (polymorphic, prototype hits, absent keys,
* misses)
* store ordinary object whose shape is entry 0's replace shape, outside
* an intact parse region, writable data entry: replace inline;
* otherwise scr_js_store_probe (adds, polymorphic, misses)
* keyed dense array element at an integral number key: inline;
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
* method dynInvoke's method read: scr_js_load_probe
* literal an object literal's constant-key member: scr_js_store_probe,
* else scr_dyn_key_set
*
* A probe that cannot answer from plain data leaves everything untouched,
* and the site calls its helper (or the generic runtime entry), which
@@ -384,6 +387,15 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string)
incoming.push(`[ ${numberResult(host, count, e)}, %${B.currentLabel()} ]`);
B.br(join);
B.startBlock(other);
// So is the length of a native array passed in from typed code.
const rest = B.newLabel("icg.rest");
const arr = nativeArray(host, receiver.name, rest);
const native = load(host, "i32", arr, 8);
const nativeCount = B.tmp();
B.line(`${nativeCount} = uitofp i32 ${native} to double`);
incoming.push(`[ ${numberResult(host, nativeCount, e)}, %${B.currentLabel()} ]`);
B.br(join);
B.startBlock(rest);
}
// Inline hits (borrowed values) and probe hits (owned ones) share one
// conversion to the result type.
@@ -393,9 +405,14 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string)
B.startBlock(object);
inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe);
B.startBlock(probe);
host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`);
// The read's probe also answers keys the receiver's plain chain lacks.
// Only `length` and `next` reads can be answered for a native array or
// iterator, so only those sites take the probe that tests them.
const probeFn =
key === "length" || key === "next" ? "scr_js_get_probe_native" : "scr_js_get_probe";
host.declare(`declare ptr @${probeFn}(ptr, ptr, ptr)`);
const probed = B.tmp();
B.line(`${probed} = call ptr @scr_js_load_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
B.line(`${probed} = call ptr @${probeFn}(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
const found = B.tmp();
B.line(`${found} = icmp ne ptr ${probed}, null`);
const probeEnd = B.currentLabel();
@@ -623,6 +640,98 @@ function numberKey(host: Host, key: IrExpr): NumberKey | null {
};
}
/** Branches to `yes` when `d` is a capsule of a native array passed in
* from typed code (a TYPED_REF bound to live array ops, not a record or
* class instance), with the ScrArr in `yes`; else to `no`. */
function nativeArray(host: Host, d: string, no: string): string {
const B = host.B;
const capsule = B.newLabel("ica.cap");
const bound = B.newLabel("ica.ops");
const yes = B.newLabel("ica.arr");
kindIs(host, d, DYN_KIND.TYPED_REF, capsule, no);
B.startBlock(capsule);
// The flag word after `casts`: record, klass and bag_class are its top
// three bits; any of them means `array` holds other ops.
const flags = load(host, "i32", d, 88);
const kinds = B.tmp();
const plain = B.tmp();
B.line(`${kinds} = lshr i32 ${flags}, 29`);
B.line(`${plain} = icmp eq i32 ${kinds}, 0`);
B.condBr(plain, bound, no);
B.startBlock(bound);
const ops = load(host, "ptr", d, 64);
const live = B.tmp();
B.line(`${live} = icmp ne ptr ${ops}, null`);
B.condBr(live, yes, no);
B.startBlock(yes);
return load(host, "ptr", d, 16);
}
/** `xs[i]` on a native number[] passed in from typed code: an integral
* index below the array's hole-free prefix reads the element in place.
* The other cases branch to `miss`, whose probe and helper answer them as
* before. The result joins `incoming` at `join`. */
function emitNativeNumberElement(
host: Host,
receiver: string,
e: IrExpr,
indexIn: (next: string) => string,
miss: string,
join: string,
incoming: string[],
): void {
const B = host.B;
const arr = nativeArray(host, receiver, miss);
const numbers = B.newLabel("ica.f64");
const elem = load(host, "i8", arr, 16);
const f64 = B.tmp();
B.line(`${f64} = icmp eq i8 ${elem}, 0 ; SCR_ELEM_F64`);
B.condBr(f64, numbers, miss);
B.startBlock(numbers);
const index = indexIn(B.newLabel("ica.idx"));
const filled = load(host, "i32", arr, 20);
const filledD = B.tmp();
const nonNegative = B.tmp();
const below = B.tmp();
const ok = B.tmp();
B.line(`${filledD} = uitofp i32 ${filled} to double`);
B.line(`${nonNegative} = fcmp oge double ${index}, 0.0`);
B.line(`${below} = fcmp olt double ${index}, ${filledD}`);
B.line(`${ok} = and i1 ${nonNegative}, ${below}`);
const ranged = B.newLabel("ica.rng");
B.condBr(ok, ranged, miss);
B.startBlock(ranged);
const i = B.tmp();
const back = B.tmp();
const integral = B.tmp();
B.line(`${i} = fptoui double ${index} to i64`);
B.line(`${back} = uitofp i64 ${i} to double`);
B.line(`${integral} = fcmp oeq double ${back}, ${index}`);
const hit = B.newLabel("ica.hit");
B.condBr(integral, hit, miss);
B.startBlock(hit);
const data = load(host, "ptr", arr, 24);
const slot = B.tmp();
const value = B.tmp();
B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${i}`);
B.line(`${value} = load double, ptr ${slot}`);
let out: string;
if (e.type.kind === "jsval") {
const raw = B.tmp();
const nan = B.tmp();
const bits = B.tmp();
out = B.tmp();
B.line(`${raw} = bitcast double ${value} to i64`);
B.line(`${nan} = fcmp uno double ${value}, ${value}`);
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
B.line(`${out} = inttoptr i64 ${bits} to ptr`);
} else {
out = numberResult(host, value, e);
}
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
B.br(join);
}
function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
const B = host.B;
const receiver = emitBorrowedInput(host, e.args[0]!);
@@ -645,18 +754,23 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
B.line(`store ptr null, ptr ${keySlot}`);
host.ownSlot(keySlot, DYN);
}
kindIs(host, receiver.name, DYN_KIND.ARR, array, probe);
/** The key as a double in block `next`; other keys go to the probe. */
const indexIn = (next: string): string => {
if (numeric === null) {
kindIs(host, key!.name, DYN_KIND.NUM, next, probe);
B.startBlock(next);
return load(host, "double", key!.name, 16);
}
B.condBr(numeric.present, next, probe);
B.startBlock(next);
return numeric.value;
};
const other = B.newLabel("ick.other");
kindIs(host, receiver.name, DYN_KIND.ARR, array, other);
B.startBlock(other);
emitNativeNumberElement(host, receiver.name, e, indexIn, probe, join, incoming);
B.startBlock(array);
let index: string;
if (numeric === null) {
kindIs(host, key!.name, DYN_KIND.NUM, number, probe);
B.startBlock(number);
index = load(host, "double", key!.name, 16);
} else {
B.condBr(numeric.present, number, probe);
B.startBlock(number);
index = numeric.value;
}
const index = indexIn(number);
const len = load(host, "i64", receiver.name, 16);
const i = B.tmp();
const back = B.tmp();
@@ -730,6 +844,27 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
return host.own({ name: out, type: e.type });
}
/** One member of an object literal (ir/js-ic.ts jsIcLiteralKey) added to
* the fresh object `obj` through the site's store cache, else with
* scr_dyn_key_set. The key and value are borrowed. */
export function emitJsIcLiteralMember(host: Host, obj: string, key: string, value: string): void {
const B = host.B;
const ic = host.jsIcSite();
host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`);
const done = B.tmp();
B.line(
`${done} = call zeroext i1 @scr_js_store_probe(ptr ${obj}, ptr ${key}, ptr ${value}, ptr ${ic})`,
);
const slow = B.newLabel("icl.slow");
const join = B.newLabel("icl.j");
B.condBr(done, join, slow);
B.startBlock(slow);
B.line(`call void @scr_dyn_key_set(ptr ${obj}, ptr ${key}, ptr ${value})`);
host.emitPendingCheck();
B.br(join);
B.startBlock(join);
}
/** dynInvoke's method read on an ordinary object through the site's
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
@@ -73,6 +73,26 @@ export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[
return [ptr(data), { type: host.sizeType, name: length }];
}
/** A TLS client's `ca` as rawBytes, with a null pointer for none: an empty
* string, which Node reads as no ca (and which an omitted ca lowers to).
* A Buffer's pointer is never null, so an empty one is an empty trust
* store. */
export function caBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
const B = host.B;
const [data, length] = rawBytes(host, value);
const test = B.tmp(),
pointer = B.tmp();
if (value.type.kind === "string") {
B.line(`${test} = icmp eq ${host.sizeType} ${length!.name}, 0`);
B.line(`${pointer} = select i1 ${test}, ptr null, ptr ${data!.name}`);
} else {
// An empty Buffer's data pointer may be null; its object stands in.
B.line(`${test} = icmp eq ptr ${data!.name}, null`);
B.line(`${pointer} = select i1 ${test}, ptr ${value.name}, ptr ${data!.name}`);
}
return [ptr(pointer), length!];
}
export function callbackType(value: LlValue): IrType & { kind: "func" } {
if (value.type.kind !== "func")
throw new InternalCompilerError("runtime callback must be a function");

Some files were not shown because too many files have changed in this diff Show More