mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge remote-tracking branch 'origin/fix/js-dynamic-lowering' into HEAD
# Conflicts: # packages/compiler/src/frontend/pipeline.ts # packages/compiler/src/frontend/shape-inference/overlay.ts # packages/compiler/src/frontend/shape-inference/static-objects.ts # packages/compiler/test/shape-overlay-render.test.ts
This commit is contained in:
@@ -622,6 +622,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
|
||||
@@ -716,8 +718,9 @@ jobs:
|
||||
# Shard 1 additionally runs package/install and link-parity contracts
|
||||
# before its LLVM differential slice. The slice itself can take ~10
|
||||
# minutes on a cold hosted runner, so 20 minutes cancels an otherwise
|
||||
# healthy job before the differential completes.
|
||||
timeout-minutes: 35
|
||||
# healthy job before the differential completes. Slow runners need more
|
||||
# headroom than that, so the limit is 50 minutes.
|
||||
timeout-minutes: 50
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Prune caches
|
||||
|
||||
# Pull request caches are scoped to that pull request, so once it is closed no
|
||||
# run can use them again. Deleting them keeps the repository under its cache
|
||||
# size limit, so caches that main and open pull requests use are not evicted.
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
prune:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Delete caches of closed pull requests
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Read both lists in full before deleting anything: deleting while the cache
|
||||
# listing pages shifts later pages, and a failed request must stop the job
|
||||
# instead of leaving open pull requests out of the list.
|
||||
open=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number')
|
||||
caches=$(gh api --paginate "repos/$REPO/actions/caches?per_page=100" \
|
||||
--jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"')
|
||||
while read -r id ref; do
|
||||
[ -n "$id" ] || continue
|
||||
number=$(echo "$ref" | cut -d/ -f3)
|
||||
if ! grep -qx "$number" <<<"$open"; then
|
||||
gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null
|
||||
echo "deleted cache $id ($ref)"
|
||||
fi
|
||||
done <<<"$caches"
|
||||
@@ -100,10 +100,59 @@ jobs:
|
||||
if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`);
|
||||
NODE
|
||||
|
||||
build-native-packages:
|
||||
name: Build native package (${{ matrix.platform }})
|
||||
# Collects the native compiler's profile on this commit
|
||||
# (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes
|
||||
# with a profile of the code it compiles. The committed profile falls behind
|
||||
# main within a day or two. If this job fails, the CLI builds use the
|
||||
# committed profile.
|
||||
cli-profile:
|
||||
name: Collect the native compiler profile
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/zig
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: linux-x64-gnu
|
||||
- run: pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
- name: Check out tsc-ts
|
||||
run: |
|
||||
git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts"
|
||||
echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)"
|
||||
npm ci --prefix "$RUNNER_TEMP/tsc-ts"
|
||||
- name: Collect the profile
|
||||
run: >-
|
||||
node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
build-native-packages:
|
||||
name: Build native package (${{ matrix.platform }})
|
||||
needs: [check-release, cli-profile]
|
||||
# Runs when the profile job fails; the CLI builds then use the committed profile.
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' &&
|
||||
needs.check-release.outputs.reuse_artifacts != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -243,6 +292,28 @@ jobs:
|
||||
- name: Build host helper for cross-libc bootstrap
|
||||
if: matrix.seed_helper != ''
|
||||
run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native
|
||||
- name: Download the profile collected on this commit
|
||||
id: cli-profile
|
||||
if: matrix.cli != ''
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile
|
||||
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and
|
||||
# build-native-cli.mts uses the committed profile.
|
||||
- name: Select the native compiler profile
|
||||
if: matrix.cli != ''
|
||||
shell: bash
|
||||
env:
|
||||
COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }}
|
||||
run: |
|
||||
if [ -n "$COLLECTED" ]; then
|
||||
echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV"
|
||||
echo "The native CLI builds with the profile collected on $GITHUB_SHA"
|
||||
else
|
||||
echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile"
|
||||
fi
|
||||
- name: Build native CLI distribution
|
||||
if: matrix.cli != ''
|
||||
env:
|
||||
@@ -558,6 +629,21 @@ jobs:
|
||||
- name: Package standalone compilers
|
||||
run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets
|
||||
|
||||
# The profile the native CLIs were built with. Runs that reuse native
|
||||
# builds from before the cli-profile job, or whose profile job failed,
|
||||
# have none; their CLIs used the committed profile.
|
||||
- name: Download the native compiler profile
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUN: ${{ needs.check-release.outputs.artifact_run }}
|
||||
VERSION: ${{ needs.check-release.outputs.version }}
|
||||
run: |
|
||||
if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then
|
||||
mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz"
|
||||
else
|
||||
echo "Run $RUN has no cli-profile artifact"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
|
||||
@@ -9,6 +9,7 @@ node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/coverage-fixtures/node-islands*/node_modules/
|
||||
!tests/coverage-fixtures/dynamic-tier/package/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ CI (`.github/workflows/release.yml`) compares the version in `packages/cli/packa
|
||||
|
||||
The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds.
|
||||
|
||||
Release builds of the native CLI optimize the compiler with the committed profile `packages/compiler/native/cli.profdata` on every platform. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. Refresh the profile on a quiet Linux x64 host when compiler code has changed substantially since the last refresh: `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Commit the new profile on its own.
|
||||
Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc-<version>-cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` on a quiet Linux x64 host and commit the new profile on its own.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
|
||||
Each published package must have a GitHub Actions trusted publisher for
|
||||
|
||||
@@ -10,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 |
|
||||
@@ -30,10 +32,12 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
|
||||
| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves |
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/date-math.ts",
|
||||
"args": [
|
||||
"40"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
|
||||
// UTC and local setters, weekday and month-end rules read back through the
|
||||
// getters, Dates compared and sorted by time value, and ISO text for a
|
||||
// sample. Argument: the number of rounds.
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "40");
|
||||
let seed = 2026;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
/** The next business day at 09:00 local time on or after `from`. */
|
||||
function nextBusinessDay(from: Date): Date {
|
||||
const day = new Date(from.getTime());
|
||||
day.setHours(9, 0, 0, 0);
|
||||
if (day < from) day.setDate(day.getDate() + 1);
|
||||
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
|
||||
return day;
|
||||
}
|
||||
|
||||
/** The last day of `date`'s month, in UTC. */
|
||||
function monthEnd(date: Date): number {
|
||||
const end = new Date(date.getTime());
|
||||
end.setUTCDate(1);
|
||||
end.setUTCMonth(end.getUTCMonth() + 1);
|
||||
end.setUTCDate(0);
|
||||
return end.getUTCDate();
|
||||
}
|
||||
|
||||
let checksum = 0;
|
||||
let sample = "";
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const due: Date[] = [];
|
||||
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
|
||||
const next = nextBusinessDay(cursor);
|
||||
checksum += next.getDate() + monthEnd(next) + next.getHours();
|
||||
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
|
||||
due.push(next);
|
||||
}
|
||||
due.sort((a, b) => +a - +b);
|
||||
let previous = due[0]!;
|
||||
for (const date of due) {
|
||||
checksum += (+date - +previous) / 3600000;
|
||||
previous = date;
|
||||
}
|
||||
sample = due[due.length >> 1]!.toISOString();
|
||||
}
|
||||
console.log(Math.round(checksum), sample);
|
||||
@@ -0,0 +1,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/throw-chains.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Exceptions that cross many frames: throws from helpers below a recursion,
|
||||
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
|
||||
// callback), with each frame holding an array across its call, and a
|
||||
// backtracking parser that uses exceptions for control flow.
|
||||
class ParseError extends Error {
|
||||
readonly at: number;
|
||||
constructor(at: number, message: string) {
|
||||
super(message);
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
/** A thrown value that is not an Error (no stack to capture). */
|
||||
class Stop {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const held: number[][] = [];
|
||||
function fail(n: number, plain: boolean): number {
|
||||
if (plain) throw new Stop(n);
|
||||
throw new RangeError("chain " + n);
|
||||
}
|
||||
function check(n: number, plain: boolean): number {
|
||||
return (n & 3) === 0 ? fail(n, plain) : n;
|
||||
}
|
||||
function validate(n: number, plain: boolean): number {
|
||||
return check(n, plain) + (n & 1);
|
||||
}
|
||||
function bottom(n: number, plain: boolean): number {
|
||||
const box = [n, -1];
|
||||
const r = validate(n, plain);
|
||||
held[0] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function walk(n: number, depth: number, plain: boolean): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
|
||||
function chains(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
for (const depth of [2, 5, 20])
|
||||
for (const plain of [false, true]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += walk(i, depth - 2, plain);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof Stop ? error.code & 7 : 1;
|
||||
}
|
||||
}
|
||||
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// The same chains through a callback: each level visits the next with a
|
||||
// closure, as tree visitors do.
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function visits(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (const depth of [2, 5, 20]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += visit(i, depth - 2, each);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof RangeError ? 1 : 0;
|
||||
}
|
||||
}
|
||||
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
|
||||
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
|
||||
// A call and a name start alike: the parser tries the call first and
|
||||
// backtracks when it fails.
|
||||
let text = "";
|
||||
let pos = 0;
|
||||
function peek(): string {
|
||||
return pos < text.length ? text[pos]! : "";
|
||||
}
|
||||
function expect(c: string): void {
|
||||
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
|
||||
pos++;
|
||||
}
|
||||
function name(): string {
|
||||
const start = pos;
|
||||
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
|
||||
if (pos === start) throw new ParseError(pos, "expected a name");
|
||||
return text.slice(start, pos);
|
||||
}
|
||||
function call(): number {
|
||||
const callee = name();
|
||||
expect("(");
|
||||
let value = expr() + callee.length;
|
||||
while (peek() === ",") {
|
||||
pos++;
|
||||
value += expr();
|
||||
}
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
function atom(): number {
|
||||
const c = peek();
|
||||
if (c >= "0" && c <= "9") {
|
||||
let value = 0;
|
||||
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
|
||||
return value;
|
||||
}
|
||||
if (c === "(") {
|
||||
pos++;
|
||||
const value = expr();
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
const start = pos;
|
||||
try {
|
||||
return call();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
pos = start;
|
||||
return name().length;
|
||||
}
|
||||
}
|
||||
function term(): number {
|
||||
let value = atom();
|
||||
while (peek() === "*") {
|
||||
pos++;
|
||||
value = (value * atom()) % 1000003;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function expr(): number {
|
||||
let value = term();
|
||||
while (peek() === "+" || peek() === "-") {
|
||||
const op = text[pos++];
|
||||
value = op === "+" ? value + term() : value - term();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
let seed = 4242;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function source(depth: number): string {
|
||||
const r = random();
|
||||
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
|
||||
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
|
||||
if (r < 0.6) return `(${source(depth + 1)})`;
|
||||
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
|
||||
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
|
||||
}
|
||||
|
||||
function parse(count: number): string {
|
||||
let total = 0;
|
||||
let failed = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Every tenth input lacks its last character, which fails deep inside it.
|
||||
const full = source(0);
|
||||
text = i % 10 === 0 ? full.slice(0, -1) : full;
|
||||
pos = 0;
|
||||
try {
|
||||
total = (total + expr()) % 1000003;
|
||||
if (pos !== text.length) throw new ParseError(pos, "trailing input");
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return `parsed ${count}: total ${total} failed ${failed}`;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
console.log(chains(Math.floor(60000 * scale)));
|
||||
console.log(visits(Math.floor(60000 * scale)));
|
||||
console.log(parse(Math.floor(40000 * scale)));
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/union-node-sets.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Syntax-tree passes over a discriminated union of object literals, the way
|
||||
// compilers walk their IR: a DAG of shared subexpressions, a `Set` of
|
||||
// visited nodes, a `Map` from node to its value, and conversions between
|
||||
// the whole union and the sub-unions passes narrow it to.
|
||||
type Num = { kind: "num"; value: number };
|
||||
type Var = { kind: "var"; slot: number };
|
||||
type Neg = { kind: "neg"; operand: Node };
|
||||
type Add = { kind: "add"; left: Node; right: Node };
|
||||
type Mul = { kind: "mul"; left: Node; right: Node };
|
||||
type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node };
|
||||
type Leaf = Num | Var;
|
||||
type Node = Leaf | Neg | Add | Mul | Pick;
|
||||
|
||||
function children(node: Node, visit: (child: Node) => void): void {
|
||||
switch (node.kind) {
|
||||
case "num":
|
||||
case "var":
|
||||
return;
|
||||
case "neg":
|
||||
visit(node.operand);
|
||||
return;
|
||||
case "add":
|
||||
case "mul":
|
||||
visit(node.left);
|
||||
visit(node.right);
|
||||
return;
|
||||
case "pick":
|
||||
visit(node.cond);
|
||||
visit(node.then);
|
||||
visit(node.else_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function leafValue(leaf: Leaf, env: number[]): number {
|
||||
return leaf.kind === "num" ? leaf.value : env[leaf.slot]!;
|
||||
}
|
||||
|
||||
function evaluate(node: Node, env: number[], memo: Map<Node, number>): number {
|
||||
const cached = memo.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
let result: number;
|
||||
switch (node.kind) {
|
||||
case "num":
|
||||
case "var":
|
||||
result = leafValue(node, env);
|
||||
break;
|
||||
case "neg":
|
||||
result = -evaluate(node.operand, env, memo);
|
||||
break;
|
||||
case "add":
|
||||
result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003;
|
||||
break;
|
||||
case "mul":
|
||||
result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003;
|
||||
break;
|
||||
case "pick":
|
||||
result =
|
||||
evaluate(node.cond, env, memo) % 2 === 0
|
||||
? evaluate(node.then, env, memo)
|
||||
: evaluate(node.else_, env, memo);
|
||||
break;
|
||||
}
|
||||
memo.set(node, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function reachable(root: Node): number {
|
||||
const seen = new Set<Node>();
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (seen.has(node)) continue;
|
||||
seen.add(node);
|
||||
children(node, (child) => {
|
||||
if (!seen.has(child)) stack.push(child);
|
||||
});
|
||||
}
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
function leaves(root: Node): Leaf[] {
|
||||
const out: Leaf[] = [];
|
||||
const seen = new Set<Node>();
|
||||
const walk = (node: Node): void => {
|
||||
if (seen.has(node)) return;
|
||||
seen.add(node);
|
||||
if (node.kind === "num" || node.kind === "var") out.push(node);
|
||||
else children(node, walk);
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
let seed = 2463534242;
|
||||
function next(): number {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >>> 17;
|
||||
seed ^= seed << 5;
|
||||
return seed >>> 0;
|
||||
}
|
||||
|
||||
function build(size: number): Node {
|
||||
const pool: Node[] = [];
|
||||
for (let i = 0; i < 64; i++)
|
||||
pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 });
|
||||
// Operands come from the most recent nodes, so most of the DAG is
|
||||
// reachable from the root and many nodes are shared.
|
||||
const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!;
|
||||
for (let i = 0; i < size; i++) {
|
||||
const a = recent();
|
||||
const b = recent();
|
||||
const r = next() % 10;
|
||||
let node: Node;
|
||||
if (r < 1) node = { kind: "neg", operand: a };
|
||||
else if (r < 5) node = { kind: "add", left: a, right: b };
|
||||
else if (r < 8) node = { kind: "mul", left: a, right: b };
|
||||
else node = { kind: "pick", cond: a, then: b, else_: recent() };
|
||||
pool.push(node);
|
||||
}
|
||||
return pool[pool.length - 1]!;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const rounds = Math.max(1, Math.round(40 * scale));
|
||||
let checksum = 0;
|
||||
let nodes = 0;
|
||||
let leafCount = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const root = build(20_000);
|
||||
nodes += reachable(root);
|
||||
const found = leaves(root);
|
||||
leafCount += found.length;
|
||||
for (let pass = 0; pass < 4; pass++) {
|
||||
const env: number[] = [];
|
||||
for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97);
|
||||
checksum = (checksum + evaluate(root, env, new Map())) % 1000003;
|
||||
for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003;
|
||||
}
|
||||
}
|
||||
console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum);
|
||||
@@ -210,6 +210,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER</code></dt>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER_SERVER</code></dt>
|
||||
<dd>Set to <code>1</code> on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves <code>tsc-ts --api --listen</code>. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (<code>TSC_TS_LISTEN_MEMORY</code> sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
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.
|
||||
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.
|
||||
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
|
||||
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Unions of a Date with other non-nullish types are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
- **Date:** Spread constructor arguments and locale formatters are unsupported.
|
||||
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
|
||||
A Date converted to `unknown` is the same Date object.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile.
|
||||
Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale).
|
||||
In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error.
|
||||
Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class.
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`.
|
||||
`Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
|
||||
The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
|
||||
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
|
||||
Class constructors are not supported.
|
||||
A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#<Object>'` (or `of function '…'`) suffix.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
|
||||
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
|
||||
The inherited methods and accessors run with the inheriting object as `this`.
|
||||
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
|
||||
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
|
||||
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
|
||||
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
`instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
|
||||
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
|
||||
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
|
||||
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
|
||||
The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
|
||||
Later `pipeline` stages and `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
|
||||
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
|
||||
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
|
||||
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
|
||||
@@ -14,7 +15,7 @@
|
||||
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
|
||||
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
The source may also be an array, a string, a `Set`, a `Map`, a generator or async generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
Stages may take the `{ signal }` argument.
|
||||
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
|
||||
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
- **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup.
|
||||
Mapper-less checked `Array.from` accepts those iterators and plain array-like objects.
|
||||
Sparse checked arrays and general borrowed iterator factories are unsupported.
|
||||
Direct statically typed class loops also restrict iterator `return` and `throw` methods.
|
||||
Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported.
|
||||
Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`.
|
||||
`for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record.
|
||||
A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`.
|
||||
Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options.
|
||||
The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options.
|
||||
`Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported.
|
||||
|
||||
@@ -2,5 +2,12 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro
|
||||
Direct `createRequire` calls to local addons are rejected at compile time.
|
||||
A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI.
|
||||
|
||||
A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block.
|
||||
There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error.
|
||||
Its `require.resolve` read as a value is a function that throws when called.
|
||||
Coverage lists these as deferred to runtime.
|
||||
The package's `catch` clause then runs as it does in Node.js when the addon is missing.
|
||||
Outside a `try` block, the same uses keep the package refused.
|
||||
|
||||
Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive.
|
||||
The guide includes a replacement for a small Node-API helper.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known.
|
||||
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
|
||||
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
|
||||
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
|
||||
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
|
||||
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
|
||||
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
|
||||
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused.
|
||||
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
|
||||
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
|
||||
The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves.
|
||||
OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason.
|
||||
A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so.
|
||||
The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields.
|
||||
@@ -0,0 +1,3 @@
|
||||
TLS servers send no session tickets, so a connection to them cannot be resumed.
|
||||
A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`.
|
||||
Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two.
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19394,6 +19394,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/1358-crypto-random.ts",
|
||||
"tests/corpus/1534-crypto-hash-chain.ts",
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/2556-crypto-introspection.ts",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
"tests/corpus/5250-cipher-aes-gcm.ts",
|
||||
@@ -19436,6 +19438,7 @@
|
||||
"tests/corpus/6709-dh-generate.ts",
|
||||
"tests/corpus/6710-dh-encodings.cjs",
|
||||
"tests/corpus/6711-dh-handles.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts",
|
||||
"tests/corpus/7055-cipher-streams.ts",
|
||||
"tests/corpus/7056-cipher-stream-errors.ts",
|
||||
"tests/corpus/7057-cipher-streams.mjs",
|
||||
@@ -19443,6 +19446,10 @@
|
||||
"tests/corpus/7082-crypto-key-objects.ts",
|
||||
"tests/corpus/7083-crypto-sign-verify.ts",
|
||||
"tests/corpus/7084-crypto-key-pairs.cjs",
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/corpus/7122-webcrypto-node-crypto.ts",
|
||||
"tests/corpus/7123-webcrypto-node-crypto.cjs",
|
||||
"tests/corpus/7150-cipher-legacy-modes.ts",
|
||||
@@ -19461,6 +19468,7 @@
|
||||
"tests/fixtures/node-types/dh.ts",
|
||||
"tests/fixtures/node-types/ecdh.ts",
|
||||
"tests/fixtures/node-types/keys.ts",
|
||||
"tests/fixtures/node-types/x509-methods.ts",
|
||||
"tests/harness/npm-static.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -23784,8 +23792,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:X509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23807,8 +23820,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkEmail",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23830,8 +23850,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkHost",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23853,8 +23880,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIP",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23876,8 +23910,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIssued",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23899,8 +23940,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkPrivateKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23922,8 +23970,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toJSON",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23945,8 +24000,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toLegacyObject",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23968,8 +24030,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toString",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23991,8 +24060,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.verify",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -24198,8 +24274,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:crypto.publicKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -32679,6 +32762,7 @@
|
||||
"tests": [
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
@@ -39012,8 +39096,11 @@
|
||||
"anchor": "err_tls_required_server_name",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -95877,6 +95964,17 @@
|
||||
"tests/corpus/6722-tls-min-max-version.ts",
|
||||
"tests/corpus/7087-tls-socket-wrap.ts",
|
||||
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts",
|
||||
"tests/fixtures/node-types/tls-socket-wrap.ts"
|
||||
]
|
||||
},
|
||||
@@ -96245,8 +96343,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.addContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96372,8 +96476,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.setSecureContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96556,8 +96666,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:'tlsClientError'",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96699,8 +96815,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96791,8 +96912,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96860,8 +96986,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96978,8 +97109,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97119,8 +97255,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.authorizationError",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97142,8 +97284,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:tls.authorized",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
|
||||
@@ -208,6 +208,8 @@
|
||||
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
|
||||
},
|
||||
"features": [
|
||||
{ "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
@@ -436,6 +438,11 @@
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] },
|
||||
{ "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] },
|
||||
@@ -446,6 +453,7 @@
|
||||
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
|
||||
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.ts"] },
|
||||
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
|
||||
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "llvm/CodeGen/MachineFunction.h"
|
||||
#include "llvm/CodeGen/MachineFunctionPass.h"
|
||||
#include "llvm/CodeGen/MachineModuleInfo.h"
|
||||
#include "llvm/IR/DiagnosticHandler.h"
|
||||
#include "llvm/IR/DiagnosticInfo.h"
|
||||
#include "llvm/IR/DiagnosticPrinter.h"
|
||||
#include "llvm/IR/LegacyPassManager.h"
|
||||
@@ -41,11 +42,13 @@
|
||||
#include "llvm/Support/SourceMgr.h"
|
||||
#include "llvm/Support/ThreadPool.h"
|
||||
#include "llvm/Support/Threading.h"
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
#include "llvm/Target/TargetMachine.h"
|
||||
#include "llvm/TargetParser/Triple.h"
|
||||
#include "llvm/Transforms/Utils/SplitModule.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -170,6 +173,108 @@ void limitProfileImports() {
|
||||
(void)Limited;
|
||||
}
|
||||
|
||||
// How the --profile-use profile matched the functions it annotated, summed
|
||||
// over partitions: functions that received its counts, functions whose
|
||||
// control flow changed since it was collected (hash mismatch), and functions
|
||||
// it has no record of (new, or never run in training: the merged profile is
|
||||
// sparse). A partition reused from the cache without its counts is counted
|
||||
// in Unknown.
|
||||
struct ProfileMatch {
|
||||
std::atomic<uint64_t> Annotated{0};
|
||||
std::atomic<uint64_t> Mismatched{0};
|
||||
std::atomic<uint64_t> Missing{0};
|
||||
std::atomic<uint64_t> Unknown{0};
|
||||
|
||||
void add(const ProfileMatch &Other) {
|
||||
Annotated += Other.Annotated;
|
||||
Mismatched += Other.Mismatched;
|
||||
Missing += Other.Missing;
|
||||
}
|
||||
|
||||
std::string serialize() const {
|
||||
return formatv("{0} {1} {2}", Annotated.load(), Mismatched.load(),
|
||||
Missing.load())
|
||||
.str();
|
||||
}
|
||||
|
||||
bool deserialize(StringRef Text) {
|
||||
SmallVector<StringRef, 3> Parts;
|
||||
Text.split(Parts, ' ');
|
||||
uint64_t Values[3];
|
||||
if (Parts.size() != 3)
|
||||
return false;
|
||||
for (size_t I = 0; I < 3; ++I)
|
||||
if (Parts[I].getAsInteger(10, Values[I]))
|
||||
return false;
|
||||
Annotated += Values[0];
|
||||
Mismatched += Values[1];
|
||||
Missing += Values[2];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Counts the profile annotation's hash-mismatch and missing-function
|
||||
// warnings instead of printing them. Every other diagnostic takes the
|
||||
// context's default handling.
|
||||
class ProfileMatchHandler : public DiagnosticHandler {
|
||||
public:
|
||||
explicit ProfileMatchHandler(ProfileMatch &Match) : Match(Match) {}
|
||||
|
||||
bool handleDiagnostics(const DiagnosticInfo &Info) override {
|
||||
if (Info.getKind() != DK_PGOProfile)
|
||||
return false;
|
||||
std::string Message;
|
||||
raw_string_ostream Stream(Message);
|
||||
DiagnosticPrinterRawOStream Printer(Stream);
|
||||
Info.print(Printer);
|
||||
if (StringRef(Message).contains("hash mismatch"))
|
||||
++Match.Mismatched;
|
||||
else if (StringRef(Message).contains_insensitive("no profile data"))
|
||||
++Match.Missing;
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
ProfileMatch &Match;
|
||||
};
|
||||
|
||||
// The annotation warns about functions the profile does not describe only
|
||||
// when asked; the match counts need those warnings. Process-wide, set once.
|
||||
void reportMissingProfiles() {
|
||||
static const bool Reported = [] {
|
||||
if (cl::Option *Option =
|
||||
cl::getRegisteredOptions().lookup("pgo-warn-missing-function"))
|
||||
Option->addOccurrence(0, "pgo-warn-missing-function", "true");
|
||||
return true;
|
||||
}();
|
||||
(void)Reported;
|
||||
}
|
||||
|
||||
// Functions of `M` that the profile annotated with counts.
|
||||
uint64_t annotatedFunctions(const Module &M) {
|
||||
uint64_t Count = 0;
|
||||
for (const Function &F : M) {
|
||||
if (F.isDeclaration())
|
||||
continue;
|
||||
std::optional<Function::ProfileCount> Entry = F.getEntryCount();
|
||||
if (Entry && !Entry->isSynthetic())
|
||||
++Count;
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
// Prints the profile match as the successful emit's one line of JSON.
|
||||
void reportProfileMatch(const ProfileMatch &Match) {
|
||||
json::Object Profile{{"annotated", Match.Annotated.load()},
|
||||
{"mismatched", Match.Mismatched.load()},
|
||||
{"missing", Match.Missing.load()},
|
||||
{"unknown_partitions", Match.Unknown.load()}};
|
||||
json::Object Response{{"ok", true}, {"profile", std::move(Profile)}};
|
||||
outs() << formatv("{0}\n", json::Value(std::move(Response)));
|
||||
}
|
||||
|
||||
// Reads the profile's header once, before any partition needs it: it must
|
||||
// hold IR-level counts.
|
||||
std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
|
||||
@@ -191,6 +296,7 @@ std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
|
||||
|
||||
// Owns the analysis managers a pipeline needs for one module.
|
||||
struct Pipeline {
|
||||
PassInstrumentationCallbacks Callbacks;
|
||||
LoopAnalysisManager LAM;
|
||||
FunctionAnalysisManager FAM;
|
||||
CGSCCAnalysisManager CGAM;
|
||||
@@ -199,13 +305,26 @@ struct Pipeline {
|
||||
|
||||
Pipeline(TargetMachine &Machine, OptimizationLevel Level,
|
||||
std::optional<PGOOptions> Profile = std::nullopt)
|
||||
: PB(&Machine, tuningFor(Level), std::move(Profile)) {
|
||||
: PB(&Machine, tuningFor(Level), std::move(Profile), &Callbacks) {
|
||||
PB.registerModuleAnalyses(MAM);
|
||||
PB.registerCGSCCAnalyses(CGAM);
|
||||
PB.registerFunctionAnalyses(FAM);
|
||||
PB.registerLoopAnalyses(LAM);
|
||||
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
|
||||
}
|
||||
|
||||
// Adds to Count the functions the profile annotation gave counts to, as
|
||||
// soon as that pass has run: inlining and dead code elimination later in
|
||||
// the pipeline delete annotated functions.
|
||||
void countAnnotated(std::atomic<uint64_t> &Count) {
|
||||
Callbacks.registerAfterPassCallback(
|
||||
[&Count](StringRef PassID, Any IR, const PreservedAnalyses &) {
|
||||
if (PassID != "PGOInstrumentationUse")
|
||||
return;
|
||||
if (const auto *M = llvm::any_cast<const Module *>(&IR))
|
||||
Count += annotatedFunctions(**M);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<EmitFailure> verify(Module &M, StringRef Code) {
|
||||
@@ -387,8 +506,15 @@ std::optional<EmitFailure> publish(StringRef TemporaryPath,
|
||||
std::optional<EmitFailure>
|
||||
emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level,
|
||||
std::optional<PGOOptions> Profile, CodeGenFileType Type,
|
||||
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries) {
|
||||
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries,
|
||||
ProfileMatch &Match) {
|
||||
bool Annotating = Profile && Profile->Action == PGOOptions::IRUse;
|
||||
if (Annotating)
|
||||
M.getContext().setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(Match));
|
||||
Pipeline P(Machine, Level, std::move(Profile));
|
||||
if (Annotating)
|
||||
P.countAnnotated(Match.Annotated);
|
||||
P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM);
|
||||
if (std::optional<EmitFailure> Failure =
|
||||
verify(M, "post_optimization_verification_failed"))
|
||||
@@ -555,20 +681,32 @@ std::optional<EmitFailure>
|
||||
compileOptimizedPartitions(const EmitOptions &Options,
|
||||
const PartitionPlan &Plan, OptimizationLevel Level,
|
||||
StringRef Salt, const ArtifactCache &Cache,
|
||||
std::vector<SmallVector<char, 0>> &Objects) {
|
||||
std::vector<SmallVector<char, 0>> &Objects,
|
||||
ProfileMatch &Match) {
|
||||
size_t Count = Plan.Modules.size();
|
||||
bool Annotating = !Options.ProfileUse.empty();
|
||||
std::vector<std::unique_ptr<MemoryBuffer>> Summaries(Count);
|
||||
SharedFailure Failures;
|
||||
forEachPartition(Count, [&](size_t I) {
|
||||
std::string Key = hashParts({Salt, "summary", Plan.Modules[I]});
|
||||
if ((Summaries[I] = Cache.lookup(Key, "bc")))
|
||||
if ((Summaries[I] = Cache.lookup(Key, "bc"))) {
|
||||
if (Annotating) {
|
||||
std::unique_ptr<MemoryBuffer> Counts = Cache.lookup(Key, "match");
|
||||
if (!Counts || !Match.deserialize(Counts->getBuffer()))
|
||||
++Match.Unknown;
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::string LookupError;
|
||||
std::unique_ptr<TargetMachine> Machine =
|
||||
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
|
||||
if (!Machine)
|
||||
return Failures.set({"target_machine_failed", LookupError});
|
||||
LLVMContext Context;
|
||||
ProfileMatch PartitionMatch;
|
||||
if (Annotating)
|
||||
Context.setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(PartitionMatch));
|
||||
Expected<std::unique_ptr<Module>> Loaded =
|
||||
loadPartition(Options, Plan.Modules[I], I, Context, *Machine);
|
||||
if (!Loaded)
|
||||
@@ -592,12 +730,19 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
// Profile instrumentation or annotation happens here, before the thin
|
||||
// link; the post-link optimization reads the annotated weights.
|
||||
Pipeline P(*Machine, Level, profileOptions(Options));
|
||||
if (Annotating)
|
||||
P.countAnnotated(PartitionMatch.Annotated);
|
||||
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
|
||||
MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false,
|
||||
/*EmitSummaryIndex=*/true,
|
||||
/*EmitModuleHash=*/true));
|
||||
MPM.run(Mod, P.MAM);
|
||||
}
|
||||
if (Annotating) {
|
||||
Match.add(PartitionMatch);
|
||||
// Stored before the summary, so a reused summary finds its counts.
|
||||
Cache.store(Key, "match", PartitionMatch.serialize());
|
||||
}
|
||||
StringRef Contents(Bitcode.data(), Bitcode.size());
|
||||
Cache.store(Key, "bc", Contents);
|
||||
Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents);
|
||||
@@ -744,7 +889,8 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
|
||||
std::optional<EmitFailure>
|
||||
compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
|
||||
std::vector<SmallVector<char, 0>> &Objects) {
|
||||
std::vector<SmallVector<char, 0>> &Objects,
|
||||
ProfileMatch &Match) {
|
||||
// Everything besides a partition's own bytes that determines its
|
||||
// artifacts. The caller's cache directory already identifies the exact
|
||||
// helper build; source content and partition count arrive per module.
|
||||
@@ -778,7 +924,7 @@ compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
|
||||
return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects);
|
||||
return compileOptimizedPartitions(Options, Plan,
|
||||
optimizationLevel(Options.OptLevel), Salt,
|
||||
Cache, Objects);
|
||||
Cache, Objects, Match);
|
||||
}
|
||||
|
||||
// Publishes in-memory partition objects through private sibling files.
|
||||
@@ -819,6 +965,8 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
std::vector<SmallVector<char, 0>> Objects;
|
||||
std::optional<EmitFailure> Failure;
|
||||
bool Compiled = false;
|
||||
// A failed attempt's counts are discarded with its objects.
|
||||
auto Match = std::make_unique<ProfileMatch>();
|
||||
{
|
||||
ErrorOr<std::unique_ptr<MemoryBuffer>> Input =
|
||||
MemoryBuffer::getFile(Options.Input, /*IsText=*/false,
|
||||
@@ -830,15 +978,16 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
Input->reset();
|
||||
PartitionPlan Plan{std::move(Text->Sources),
|
||||
std::move(Text->PublicSymbols)};
|
||||
Compiled = !compilePartitions(Options, Plan, Objects);
|
||||
Compiled = !compilePartitions(Options, Plan, Objects, *Match);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Compiled) {
|
||||
Match = std::make_unique<ProfileMatch>();
|
||||
PartitionPlan Plan;
|
||||
Failure = splitParsedModule(Options, Plan);
|
||||
if (!Failure)
|
||||
Failure = compilePartitions(Options, Plan, Objects);
|
||||
Failure = compilePartitions(Options, Plan, Objects, *Match);
|
||||
}
|
||||
std::vector<SmallString<256>> Temporaries;
|
||||
if (!Failure)
|
||||
@@ -851,6 +1000,8 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(*Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -895,6 +1046,8 @@ int emit(const EmitOptions &Options) {
|
||||
return reportError("invalid_import",
|
||||
"runtime bitcode import requires an optimized build",
|
||||
Options.DiagnosticFormat);
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportMissingProfiles();
|
||||
if (std::optional<EmitFailure> Failure = inspectProfile(Options))
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
@@ -957,9 +1110,10 @@ int emit(const EmitOptions &Options) {
|
||||
? CodeGenFileType::ObjectFile
|
||||
: CodeGenFileType::AssemblyFile;
|
||||
std::vector<SmallString<256>> Temporaries;
|
||||
ProfileMatch Match;
|
||||
std::optional<EmitFailure> Failure =
|
||||
emitModule(*Mod, *Machine, Level, profileOptions(Options), Type,
|
||||
Options.Outputs[0], Temporaries);
|
||||
Options.Outputs[0], Temporaries, Match);
|
||||
if (!Failure)
|
||||
Failure = publish(Temporaries[0], Options.Outputs[0]);
|
||||
if (Failure) {
|
||||
@@ -968,6 +1122,8 @@ int emit(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ async function tryFastPath(): Promise<number | null> {
|
||||
values.sparse ||
|
||||
// The full compiler resolves `native` against the build host.
|
||||
values["target-cpu"] !== undefined ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The full CLI validates the Node island policy flags.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
const backend = values.backend;
|
||||
@@ -187,6 +189,21 @@ async function tryFastPath(): Promise<number | null> {
|
||||
});
|
||||
}
|
||||
|
||||
function hasNodeIslandFlags(
|
||||
values: ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
>["values"],
|
||||
): boolean {
|
||||
return (
|
||||
values["node-islands"] !== undefined ||
|
||||
(values["node-island"] ?? []).length > 0 ||
|
||||
(values.native ?? []).length > 0 ||
|
||||
values.node !== undefined ||
|
||||
values["explain-island"] !== undefined ||
|
||||
values["strict-user-js"]
|
||||
);
|
||||
}
|
||||
|
||||
/** An unchanged program's coverage verdict replays from the verdict cache
|
||||
* without loading the compiler graph. Anything else (a miss, a comment-only
|
||||
* edit, options the replay does not cover) defers to the full CLI. */
|
||||
@@ -218,7 +235,9 @@ async function coverageFastPath(
|
||||
values["emit-ir"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
values["shape-overlay"] === false ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The replay covers the default Node island policy only.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
let startup: typeof import("@scriptc/compiler/startup-cache");
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
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_v43");
|
||||
});
|
||||
});
|
||||
|
||||
+100
-8
@@ -3551,17 +3551,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
|
||||
@@ -5473,6 +5501,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 +5525,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 +5543,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 +6001,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 +6043,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 +6138,8 @@ declare module "tls" {
|
||||
/** An opaque parsed cert/key pair — what an SNI callback answers with
|
||||
* (the value is a runtime handle; the members are runtime-internal). */
|
||||
export interface SecureContext {}
|
||||
/* @types/node's name for createSecureContext's options */
|
||||
export type SecureContextOptions = TlsOptions;
|
||||
export function createSecureContext(options: TlsOptions): SecureContext;
|
||||
/** The CA-store introspection surface (scr_tls_ca.c): per-type cached
|
||||
* PEM string arrays. The host bundle stands in for Node's compiled-in
|
||||
|
||||
@@ -70,3 +70,24 @@ interface PromiseConstructor {
|
||||
reject: (reason: Error) => void;
|
||||
};
|
||||
}
|
||||
|
||||
/* Carriers of the types the lowerer gives untyped JavaScript collections
|
||||
* whose uses prove them (js-collection-types.ts). Nothing reads them at run
|
||||
* time. */
|
||||
declare namespace __scriptcInferredCollections {
|
||||
const map_string_string: Map<string, string>;
|
||||
const map_string_number: Map<string, number>;
|
||||
const map_string_boolean: Map<string, boolean>;
|
||||
const map_number_string: Map<number, string>;
|
||||
const map_number_number: Map<number, number>;
|
||||
const map_number_boolean: Map<number, boolean>;
|
||||
const map_boolean_string: Map<boolean, string>;
|
||||
const map_boolean_number: Map<boolean, number>;
|
||||
const map_boolean_boolean: Map<boolean, boolean>;
|
||||
const set_string: Set<string>;
|
||||
const set_number: Set<number>;
|
||||
const set_boolean: Set<boolean>;
|
||||
const array_string: string[];
|
||||
const array_number: number[];
|
||||
const array_boolean: boolean[];
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -135,3 +135,16 @@ void scr_cyc_set_growth_cap(long cap);
|
||||
void scriptc_native_bound_cycle_growth(void) {
|
||||
scr_cyc_set_growth_cap(2);
|
||||
}
|
||||
|
||||
/* The runtime's nursery trigger (scr_rc.h). */
|
||||
void scr_cyc_set_nursery_threshold(long threshold);
|
||||
|
||||
/* Runs nursery passes every 1024 new candidates instead of every 256. Much
|
||||
* of a build's cyclic garbage outlives 256 releases: in the shorter window
|
||||
* it survives a nursery pass, is promoted, and only full passes over the
|
||||
* whole live heap find it. A constructor sets it before the compiler's
|
||||
* modules initialize, so every pass of a build uses it.
|
||||
* SCR_CYCLE_THRESHOLD still overrides it. */
|
||||
__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) {
|
||||
scr_cyc_set_nursery_threshold(1024);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Reads cases from standard input, each a little-endian u32 length, a u32
|
||||
* start and the bytes, and prints scriptc_ts7_json_value_end for each. */
|
||||
#include "ts7-process.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
static int read_u32(uint32_t *value) {
|
||||
unsigned char bytes[4];
|
||||
if (fread(bytes, 1, 4, stdin) != 4) return 0;
|
||||
*value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 |
|
||||
(uint32_t)bytes[3] << 24;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
#ifdef _WIN32
|
||||
/* The cases are bytes: text mode would rewrite line endings. */
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
#endif
|
||||
uint32_t length, start;
|
||||
while (read_u32(&length)) {
|
||||
if (!read_u32(&start)) return 1;
|
||||
uint8_t *bytes = malloc(length == 0 ? 1 : length);
|
||||
if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1;
|
||||
printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start));
|
||||
free(bytes);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -12,7 +12,49 @@
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In
|
||||
* mode "listen-exit" it exits as an executable without --listen does. In
|
||||
* mode "listen" it answers each client, in order, with its client number,
|
||||
* its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the
|
||||
* client closes, and exits after SECONDS (at most 5) without a client. */
|
||||
static int listen_main(const char *mode, char **argv) {
|
||||
if (strcmp(mode, "listen-exit") == 0) return 2;
|
||||
if (strcmp(mode, "listen") != 0) return 73;
|
||||
struct sockaddr_un address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sun_family = AF_UNIX;
|
||||
if (strlen(argv[0]) >= sizeof(address.sun_path)) return 77;
|
||||
strcpy(address.sun_path, argv[0]);
|
||||
int listener = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(listener, 4) != 0)
|
||||
return 78;
|
||||
const char *memory = getenv("TSC_TS_LISTEN_MEMORY");
|
||||
int idle = atoi(argv[2]);
|
||||
if (idle > 5) idle = 5;
|
||||
for (int client = 1;; client++) {
|
||||
struct pollfd entry = {listener, POLLIN, 0};
|
||||
if (poll(&entry, 1, idle * 1000) <= 0) break;
|
||||
int fd = accept(listener, NULL, NULL);
|
||||
if (fd < 0) return 79;
|
||||
char line[512];
|
||||
int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2],
|
||||
memory == NULL ? "unset" : memory);
|
||||
if (size <= 0 || write(fd, line, (size_t)size) != size) return 80;
|
||||
uint8_t bytes[17];
|
||||
ssize_t count;
|
||||
while ((count = read(fd, bytes, sizeof(bytes))) > 0)
|
||||
if (write(fd, bytes, (size_t)count) != count) break;
|
||||
close(fd);
|
||||
}
|
||||
unlink(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int child_main(int argc, char **argv) {
|
||||
@@ -20,9 +62,15 @@ 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;
|
||||
if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0))
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -338,6 +356,179 @@ 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, uint32_t idle_exit, uint32_t memory) {
|
||||
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory;
|
||||
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, 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[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle,
|
||||
timing ? "--timing" : NULL, 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 +596,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;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,19 @@ 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);
|
||||
/* 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, 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 +34,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle);
|
||||
void scriptc_ts7_close(uint32_t handle);
|
||||
void scriptc_ts7_close_all(void);
|
||||
int32_t scriptc_ts7_error(uint8_t *bytes, size_t size);
|
||||
/* The index after the JSON value that starts at `start` in a checker reply,
|
||||
* as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is
|
||||
* malformed there. */
|
||||
int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -171,6 +173,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, 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 +281,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); }
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../shape-reg/packages/compiler/node_modules
|
||||
@@ -19,6 +19,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
@@ -62,6 +63,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
dh: features.dh,
|
||||
pki: features.pki,
|
||||
ed25519: features.ed25519,
|
||||
x509: features.x509,
|
||||
webcrypto: features.webcrypto,
|
||||
perfLoop: features.perfLoop,
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function compilePackedLibrary(
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: options.x509 ?? false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
tlsCa: false,
|
||||
@@ -152,7 +153,7 @@ export async function compilePackedLibrary(
|
||||
...(await Promise.all(
|
||||
shards
|
||||
.slice(start, start + 4)
|
||||
.map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)),
|
||||
.map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* code after it); here the lines are DROPPED instead, and emitStmts skips
|
||||
* whole statements once the current block is terminated, so no dropped
|
||||
* definition can leak into a later block. */
|
||||
import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export class BlockBuilder {
|
||||
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
|
||||
@@ -86,7 +87,18 @@ export class BlockBuilder {
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) {
|
||||
if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue);
|
||||
if (
|
||||
this.returnEpilogue &&
|
||||
(s.startsWith("ret ") ||
|
||||
s.startsWith("call void @scr_unwind_raise()") ||
|
||||
s === UNWIND_RESUME)
|
||||
)
|
||||
this.line(this.returnEpilogue);
|
||||
if (s === UNWIND_RESUME) {
|
||||
const exception = this.tmp();
|
||||
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
|
||||
s = `resume { ptr, i32 } ${exception}`;
|
||||
}
|
||||
this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
|
||||
}
|
||||
|
||||
/** Element storage's tag, and the alias scope of the access being emitted
|
||||
* in a loop version's fast copy (bytesAccessScope). */
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:elements");
|
||||
return host.memoryAliasAttachment("bytes:elements") + host.bytesAccessScope;
|
||||
}
|
||||
|
||||
/** A module global's slot is its own category for whole-slot loads of a
|
||||
|
||||
@@ -14,11 +14,25 @@ interface Bound extends Extent {
|
||||
minimum: number;
|
||||
}
|
||||
type Node = IrExpr | IrStmt;
|
||||
const FLIPPED: Readonly<Record<string, string>> = {
|
||||
"<": ">",
|
||||
"<=": ">=",
|
||||
">": "<",
|
||||
">=": "<=",
|
||||
"===": "===",
|
||||
"==": "==",
|
||||
"!==": "!==",
|
||||
"!=": "!=",
|
||||
};
|
||||
|
||||
/** Relational proofs for fixed typed-array lengths. Cached lengths retain
|
||||
* their receiver identity only when that binding is never reassigned.
|
||||
* Loops may use offsets in either direction, but both index integrality
|
||||
* and receiver stability are established separately from the relation. */
|
||||
* and receiver stability are established separately from the relation.
|
||||
* For loops hold one bound over the body. While loops start from the
|
||||
* condition's bound and follow the body in order: increments raise it and
|
||||
* comparisons with the length in if conditions narrow it. Indexes are
|
||||
* `counter + c` or `L + d - counter`, where L is the receiver's length. */
|
||||
export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): ReadonlySet<Node> {
|
||||
const proved = new Map<Node, boolean>();
|
||||
if (fn.async || fn.generator || !fn.locals.some((local) => local.type.kind === "bytes"))
|
||||
@@ -96,6 +110,20 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
|
||||
return Number.isSafeInteger(result) ? result : null;
|
||||
}
|
||||
/** For an index of the form `L + d - counter` (`len - 1 - i`, `right - i`
|
||||
* with `right = len - 1`), where L is the receiver's length: d. */
|
||||
function reversed(e: IrExpr, bound: Bound): number | null {
|
||||
if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-")) return null;
|
||||
if (e.op === "-" && e.right.kind === "varRef" && e.right.localId === bound.counter) {
|
||||
const length = extent(e.left, false);
|
||||
return length && length.receiver === bound.receiver ? length.offset : null;
|
||||
}
|
||||
if (e.right.kind !== "numLit" || !Number.isSafeInteger(e.right.value)) return null;
|
||||
const left = reversed(e.left, bound);
|
||||
if (left === null) return null;
|
||||
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
|
||||
return Number.isSafeInteger(result) ? result : null;
|
||||
}
|
||||
function affineRange(e: IrExpr, bound: Bound): IntegerRange | null {
|
||||
if (e.kind === "varRef" && e.localId === bound.counter) {
|
||||
const max = Number.MAX_SAFE_INTEGER + bound.offset - 1;
|
||||
@@ -159,7 +187,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
if (s.cond.left.kind !== "varRef" || s.cond.left.localId !== counter) {
|
||||
if (right.kind !== "varRef" || right.localId !== counter) return null;
|
||||
right = s.cond.left;
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
op = FLIPPED[op] ?? "";
|
||||
}
|
||||
const update = s.update;
|
||||
let step = 0;
|
||||
@@ -212,6 +240,98 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
}
|
||||
return result && stable(s.body, result.receiver, counter) ? result : null;
|
||||
}
|
||||
/** A comparison of the counter with the receiver's length: the operator
|
||||
* with the counter on the left, and the length's offset. */
|
||||
function lengthComparison(e: IrExpr, bound: Bound): { op: string; offset: number } | null {
|
||||
if (e.kind !== "bin") return null;
|
||||
let other = e.right,
|
||||
op: string = e.op;
|
||||
if (e.left.kind !== "varRef" || e.left.localId !== bound.counter) {
|
||||
if (other.kind !== "varRef" || other.localId !== bound.counter) return null;
|
||||
other = e.left;
|
||||
op = FLIPPED[op] ?? op;
|
||||
}
|
||||
const length = extent(other, false);
|
||||
return length && length.receiver === bound.receiver ? { op, offset: length.offset } : null;
|
||||
}
|
||||
/** The step of `counter = counter + c` with a positive literal c, or null. */
|
||||
function increment(s: IrStmt, counter: string): number | null {
|
||||
return s.kind === "assign" &&
|
||||
s.localId === counter &&
|
||||
s.value.kind === "bin" &&
|
||||
s.value.op === "+" &&
|
||||
s.value.left.kind === "varRef" &&
|
||||
s.value.left.localId === counter &&
|
||||
s.value.right.kind === "numLit" &&
|
||||
Number.isSafeInteger(s.value.right.value) &&
|
||||
s.value.right.value > 0
|
||||
? s.value.right.value
|
||||
: null;
|
||||
}
|
||||
function writes(s: IrStmt, id: string): boolean {
|
||||
return !everyStmtList([s], {
|
||||
expr: (e) => (e.kind !== "assignExpr" && e.kind !== "incDec") || e.localId !== id,
|
||||
stmt: (child) =>
|
||||
!(
|
||||
((child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") &&
|
||||
child.localId === id) ||
|
||||
(child.kind === "tryCatch" && child.catchLocalId === id)
|
||||
),
|
||||
});
|
||||
}
|
||||
/** A while loop whose condition bounds an integer counter by a typed
|
||||
* array's length. The body may write the counter only by statements
|
||||
* that add a positive literal, and may not write the receiver. Calls
|
||||
* cannot write either binding (both are uncaptured locals), and a typed
|
||||
* array's length is fixed at construction, so the body may hold calls. */
|
||||
function whileBound(s: IrStmt & { kind: "while" }): Bound | null {
|
||||
const cond = s.cond;
|
||||
if (cond.kind !== "bin") return null;
|
||||
const counterRef = [cond.left, cond.right].find(
|
||||
(side, i) =>
|
||||
side.kind === "varRef" &&
|
||||
eligible(side.localId) &&
|
||||
extent(i === 0 ? cond.right : cond.left, false) !== null,
|
||||
);
|
||||
const minimum = counterRef ? ranges.get(counterRef)?.min : undefined;
|
||||
if (counterRef?.kind !== "varRef" || minimum === undefined) return null;
|
||||
const counter = counterRef.localId;
|
||||
const length = extent(counterRef === cond.left ? cond.right : cond.left, false)!;
|
||||
const receiver = length.receiver;
|
||||
const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum });
|
||||
if (comparison?.op !== "<" && comparison?.op !== "<=") return null;
|
||||
// An increment is a statement of the body: one inside an expression
|
||||
// (a comma expression's statements) would run where flow does not
|
||||
// follow it.
|
||||
const writesOk = everyStmtList(s.body, {
|
||||
expr: (e) =>
|
||||
((e.kind !== "assignExpr" && e.kind !== "incDec") ||
|
||||
(e.localId !== counter && e.localId !== receiver)) &&
|
||||
everyExprChild(
|
||||
e,
|
||||
() => true,
|
||||
(child) => !writes(child, counter),
|
||||
),
|
||||
stmt: (child) => {
|
||||
if (child.kind === "assign" && child.localId === counter)
|
||||
return increment(child, counter) !== null;
|
||||
if (child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl")
|
||||
return child.localId !== counter && child.localId !== receiver;
|
||||
return (
|
||||
child.kind !== "tryCatch" ||
|
||||
(child.catchLocalId !== counter && child.catchLocalId !== receiver)
|
||||
);
|
||||
},
|
||||
});
|
||||
return writesOk
|
||||
? {
|
||||
receiver,
|
||||
counter,
|
||||
offset: comparison.offset + (comparison.op === "<=" ? 1 : 0),
|
||||
minimum,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void {
|
||||
const integer = ranges.get(index);
|
||||
let safe = false;
|
||||
@@ -228,6 +348,13 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
safe = true;
|
||||
break;
|
||||
}
|
||||
// counter < L + bound.offset and counter >= bound.minimum put
|
||||
// L + d - counter in [0, L - width].
|
||||
const d = integer ? reversed(index, bound) : null;
|
||||
if (d !== null && bound.offset <= d + 1 && bound.minimum >= d + width) {
|
||||
safe = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
proved.set(node, (proved.get(node) ?? true) && safe);
|
||||
@@ -244,8 +371,84 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
|
||||
(child) => stmt(child, active),
|
||||
);
|
||||
}
|
||||
/** The bounds where `cond` is true and where it is false, from the
|
||||
* bound before it. */
|
||||
function narrow(cond: IrExpr, bound: Bound): [Bound, Bound] {
|
||||
const comparison = lengthComparison(cond, bound);
|
||||
if (!comparison) return [bound, bound];
|
||||
const j = comparison.offset,
|
||||
offset = bound.offset;
|
||||
// counter < L + j, counter <= L + j, and counter !== L + j.
|
||||
const below = { ...bound, offset: Math.min(offset, j) },
|
||||
atMost = { ...bound, offset: Math.min(offset, j + 1) },
|
||||
unequal = offset === j + 1 ? { ...bound, offset: j } : bound;
|
||||
switch (comparison.op) {
|
||||
case "<":
|
||||
return [below, bound];
|
||||
case "<=":
|
||||
return [atMost, bound];
|
||||
case ">":
|
||||
return [bound, atMost];
|
||||
case ">=":
|
||||
return [bound, below];
|
||||
case "===":
|
||||
case "==":
|
||||
return [atMost, unequal];
|
||||
case "!==":
|
||||
case "!=":
|
||||
return [unequal, atMost];
|
||||
default:
|
||||
return [bound, bound];
|
||||
}
|
||||
}
|
||||
/** Visits a while body in order, from the bound that holds on entry.
|
||||
* Returns the bound at the end of the list. Its offset is -Infinity (and
|
||||
* its minimum Infinity) when the end is unreachable, and Infinity when
|
||||
* nothing bounds the counter there. Every write of the counter adds a
|
||||
* positive literal (whileBound), so the minimum survives any write. */
|
||||
function flow(list: IrStmt[], active: Bound[], bound: Bound): Bound {
|
||||
for (const s of list) {
|
||||
const current = Number.isFinite(bound.offset) ? [...active, bound] : active;
|
||||
const step = increment(s, bound.counter);
|
||||
if (step !== null) {
|
||||
bound = { ...bound, offset: bound.offset + step, minimum: bound.minimum + step };
|
||||
} else if (s.kind === "if") {
|
||||
expr(s.cond, current);
|
||||
const [then, else_] = narrow(s.cond, bound);
|
||||
const left = flow(s.then, active, then),
|
||||
right = s.else_ ? flow(s.else_, active, else_) : else_;
|
||||
bound = {
|
||||
...bound,
|
||||
offset: Math.max(left.offset, right.offset),
|
||||
minimum: Math.min(left.minimum, right.minimum),
|
||||
};
|
||||
} else if (s.kind === "block" && !s.labels?.length) {
|
||||
bound = flow(s.body, active, bound);
|
||||
} else if (writes(s, bound.counter)) {
|
||||
stmt(s, active);
|
||||
bound = { ...bound, offset: Infinity };
|
||||
} else {
|
||||
stmt(s, current);
|
||||
if (
|
||||
s.kind === "break" ||
|
||||
s.kind === "continue" ||
|
||||
s.kind === "return" ||
|
||||
s.kind === "throw"
|
||||
)
|
||||
bound = { ...bound, offset: -Infinity, minimum: Infinity };
|
||||
}
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
function stmt(s: IrStmt, active: Bound[]): boolean {
|
||||
if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1);
|
||||
if (s.kind === "while") {
|
||||
expr(s.cond, active);
|
||||
const bound = whileBound(s);
|
||||
if (!bound) return s.body.every((child) => stmt(child, active));
|
||||
flow(s.body, active, bound);
|
||||
return true;
|
||||
}
|
||||
if (s.kind === "for") {
|
||||
if (s.init) stmt(s.init, active);
|
||||
if (s.cond) expr(s.cond, active);
|
||||
|
||||
@@ -580,7 +580,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 +701,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,17 @@ 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 {
|
||||
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" };
|
||||
@@ -73,6 +87,14 @@ export interface BytesLoopVersion {
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
ownedReceivers: number[];
|
||||
/** 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 +114,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 +142,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 +563,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 +590,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 +716,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 +752,9 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
@@ -704,6 +780,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 +1100,22 @@ 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 `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,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
@@ -1043,19 +1134,36 @@ 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);
|
||||
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]!));
|
||||
});
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
@@ -1067,6 +1175,31 @@ export function emitBytesLoopVersionGuard(
|
||||
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 +1291,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 +1325,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 +1348,83 @@ 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 (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 length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, once it is the scan receiver, the
|
||||
* published unit count (0 when the runtime does not hold its units). When
|
||||
* `s` is not already the scan receiver, the runtime converts it to units
|
||||
* and publishes it (scr_str_scan_units). Also the units pointer, whether
|
||||
* the loop is to read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: 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.
|
||||
const held = B.tmp(),
|
||||
holds = B.tmp();
|
||||
B.line(`${held} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${holds} = icmp eq ptr ${held}, ${s}`);
|
||||
const call = B.newLabel("bytes.version.publish.units"),
|
||||
load = B.newLabel("bytes.version.publish.load");
|
||||
B.condBr(holds, load, call);
|
||||
B.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();
|
||||
B.line(`call void @scr_str_scan_units(ptr ${s})`);
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ const counted = (
|
||||
guarded: false,
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
startBound: Number.MAX_SAFE_INTEGER,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
|
||||
@@ -36,7 +36,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
|
||||
import { emitCountedLoopLimit, emitExactIntegerTest } from "./counted-loops.js";
|
||||
import { emitCountedLoopLimit, emitStartIntegerTest } from "./counted-loops.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
@@ -453,7 +453,7 @@ function emitPairedDenseGuard(
|
||||
): string {
|
||||
const B = host.B;
|
||||
const start = (expr: IrExpr, min: number, max: number) =>
|
||||
emitExactIntegerTest(host, host.emitExpr(expr).name, min, max);
|
||||
emitStartIntegerTest(host, expr, min, max);
|
||||
const low = start(pair.lowStart, 0, INT32_MAX - 1);
|
||||
const high = start(pair.highStart, -1, INT32_MAX - 1);
|
||||
let valid = B.tmp();
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
recordHandleCodecClass,
|
||||
textCodecRecordClass,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
@@ -98,14 +98,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 +153,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 +188,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 +406,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 +518,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 +558,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 +581,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 +605,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 +634,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 +647,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)`,
|
||||
@@ -1474,8 +1566,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 +1595,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 +1735,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 +1758,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 +1781,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 +1826,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 +1896,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;
|
||||
@@ -1808,6 +1964,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 +2001,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} {`,
|
||||
@@ -2596,7 +2779,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 +2818,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 +2854,7 @@ export class LlDyn {
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
trustedKinds,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
@@ -2662,7 +2865,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 +3120,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 +3142,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 +3158,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 +3342,7 @@ export class LlDyn {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const codec = recordTextCodecClass(shape);
|
||||
const codec = recordHandleCodecClass(shape);
|
||||
if (codec !== null) {
|
||||
this.textCodecToDyn(B, t, shape, codec);
|
||||
break;
|
||||
|
||||
@@ -20,7 +20,12 @@ import { constructorClass } from "../../ir/final-fields.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js";
|
||||
import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js";
|
||||
import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js";
|
||||
import {
|
||||
findMapReadBorrows,
|
||||
findMapReadLifetimes,
|
||||
findReadNumberMapWrites,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { borrowableMapGet } from "./expr-containers.js";
|
||||
import { findMapEntryFusions } from "./map-entry-fusion.js";
|
||||
import { findLocalStackUnions } from "./stack-unions.js";
|
||||
import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js";
|
||||
@@ -56,12 +61,17 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name);
|
||||
emitter.usesLandingPads = false;
|
||||
emitter.aliasScopedVersions = 0;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.speculativeLeaves = 0;
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
@@ -188,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,
|
||||
@@ -597,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]`);
|
||||
}
|
||||
@@ -637,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);
|
||||
|
||||
@@ -61,7 +61,7 @@ import { hasLoneSurrogate, wtf8Bytes } from "../literals.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
|
||||
import { literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
@@ -102,10 +102,10 @@ function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
|
||||
return fnDefs;
|
||||
}
|
||||
|
||||
/** Modules with fewer functions emit in one process: forking and merging
|
||||
* would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS
|
||||
* lowers it, for tests). */
|
||||
function workerMinFunctions(): number {
|
||||
/** Modules with fewer functions emit in one process, and compute their
|
||||
* analyses in it: forking and merging would cost more than the workers save
|
||||
* (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */
|
||||
export function workerMinFunctions(): number {
|
||||
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
|
||||
return Number.isFinite(value) && value > 0 ? value : 2000;
|
||||
}
|
||||
@@ -1014,7 +1014,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());
|
||||
@@ -1160,7 +1160,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
: [` %native_exit = call i32 @scr_exit_code_hint_get()`, ` ret i32 %native_exit`]),
|
||||
`}`,
|
||||
``,
|
||||
...emitter.inlineRcTail(flushedDecls),
|
||||
);
|
||||
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
|
||||
out.push(
|
||||
// sanitize_address is inert under the plain pipeline; the sanitized
|
||||
// lane's -fsanitize=address link activates instrumentation over the
|
||||
// emitted functions too (the runtime TUs get theirs from clang).
|
||||
@@ -1299,6 +1301,42 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word, its
|
||||
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
|
||||
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
|
||||
@@ -1615,9 +1653,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;
|
||||
|
||||
@@ -32,7 +32,7 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js";
|
||||
import { boxAccess, boxNewCall, elemAccess } from "./shapes.js";
|
||||
import { dynHoleRead } from "./loop-array-borrows.js";
|
||||
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
|
||||
import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { charAtUnitRead, emitBorrowedMapGet, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
import {
|
||||
emitCountedLoopLimit,
|
||||
emitCountedLoopSafety,
|
||||
emitCountedStart,
|
||||
emitPairedLoopSafety,
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js";
|
||||
@@ -104,6 +105,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;
|
||||
@@ -570,6 +584,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.value,
|
||||
emitter.bytesBounds.has(s),
|
||||
emitter.bytesOwned.has(s),
|
||||
emitter.bytesAliasScopes.get(s),
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1112,9 +1127,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 +1143,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 +1160,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;
|
||||
@@ -2065,7 +2074,8 @@ function emitCallbackVersionedLoop(
|
||||
/** 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. */
|
||||
* 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;
|
||||
@@ -2080,6 +2090,46 @@ function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
* 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
|
||||
@@ -2096,28 +2146,69 @@ 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 guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast);
|
||||
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);
|
||||
emitSlowCopy(emitter, 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 (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);
|
||||
}
|
||||
@@ -2151,6 +2242,7 @@ export function emitLoopCopies(
|
||||
);
|
||||
},
|
||||
(id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id),
|
||||
emitter.speculationContext(),
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
@@ -2209,6 +2301,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);
|
||||
@@ -2234,6 +2340,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"));
|
||||
@@ -2242,6 +2349,7 @@ export function emitLoopCopies(
|
||||
emitCopy(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationResumeValues = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
B.startBlock(fast);
|
||||
@@ -2290,6 +2398,7 @@ export function emitLoopCopies(
|
||||
deferred,
|
||||
bails,
|
||||
ran,
|
||||
values: plan.values,
|
||||
};
|
||||
try {
|
||||
const leave = B.newLabel("spec.leave");
|
||||
@@ -2547,9 +2656,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([]);
|
||||
@@ -2564,7 +2686,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");
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { youngNurseryRequested } from "../young-nursery.js";
|
||||
import { llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { serializeModuleParts } from "../../ir/serialize.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
endsWithJump,
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
typeKey,
|
||||
VOID,
|
||||
classJsName,
|
||||
classValueAbiEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
@@ -68,7 +70,7 @@ import {
|
||||
type StoredSplitSnapshot,
|
||||
type StringSplit,
|
||||
} from "./split-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { scalarizeLiteralResults, scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries, resultEntryName } from "./record-results.js";
|
||||
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
|
||||
import {
|
||||
@@ -80,11 +82,15 @@ import {
|
||||
} from "./emit-journal.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import { inlineExpressionFunctions } from "../../ir/expression-inlining.js";
|
||||
import { pairRowSums } from "../../ir/row-pairs.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
int32ArrayKey,
|
||||
selectInt32Arrays,
|
||||
type Int32ArrayProof,
|
||||
type Int32ArrayRevision,
|
||||
} from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import {
|
||||
@@ -100,8 +106,9 @@ import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import {
|
||||
analyzeInt32Slots,
|
||||
dynamicFieldStores,
|
||||
Int32RevisionConflict,
|
||||
type DynamicFieldStores,
|
||||
type Int32Slots,
|
||||
Int32Slots,
|
||||
} from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -109,6 +116,8 @@ import { dropUnwrittenPropertyBags } from "../../ir/property-bags.js";
|
||||
import { freezeInitOnlyGlobals } from "../../ir/init-only-globals.js";
|
||||
import { hoistFinalFieldReads } from "../../ir/field-values.js";
|
||||
import { holdCharLocalsAsUnits } from "../../ir/char-unit-locals.js";
|
||||
import { unitSafeFunctions } from "./string-unit-versions.js";
|
||||
import { stringScanInline } from "./string-reads.js";
|
||||
import {
|
||||
findInt32Abis,
|
||||
INT32_ABI_SUFFIX,
|
||||
@@ -118,7 +127,7 @@ import {
|
||||
} from "./int32-abi.js";
|
||||
import { type Int32Local } from "./int32-locals.js";
|
||||
import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js";
|
||||
import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js";
|
||||
import { exactInteger, narrowInteger, widenInteger, type LlInteger } from "./integer-values.js";
|
||||
import { findInitializerBindings } from "../../ir/initializer-bindings.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
import {
|
||||
@@ -128,6 +137,16 @@ import {
|
||||
type FfiCallbackAdapter,
|
||||
} from "../ffi-callbacks.js";
|
||||
import { computeMayThrow } from "../may-throw.js";
|
||||
import {
|
||||
UNWIND_PERSONALITY,
|
||||
UNWIND_RAISE,
|
||||
UNWIND_RAISE_DECLARATION,
|
||||
UNWIND_RESUME,
|
||||
UNWIND_SLOT,
|
||||
UnwindAbiViolation,
|
||||
bridgePlainUnwindCalls,
|
||||
unwindAbiFunctions,
|
||||
} from "./unwind-abi.js";
|
||||
import {
|
||||
mangleAsyncInlineAwaitCall,
|
||||
mangleAsyncSpawn,
|
||||
@@ -186,8 +205,10 @@ import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
findLentStringParameters,
|
||||
type LentStringParameters,
|
||||
findRecordFieldProducers,
|
||||
type LendingFacts,
|
||||
type StringSliceSnapshot,
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
@@ -343,13 +364,22 @@ import {
|
||||
} from "./shapes.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
|
||||
import { INLINE_LITERAL_BYTES } from "./string-literal-equality.js";
|
||||
import {
|
||||
inlinableNumberFunction,
|
||||
withSpeculativeValue,
|
||||
type InlinableFunction,
|
||||
type SpeculationContext,
|
||||
type SpeculativeValue,
|
||||
} from "./speculative-integers.js";
|
||||
import {
|
||||
emitFfiCallbackDefs,
|
||||
emitLibDefs,
|
||||
emitModuleParts,
|
||||
emitParts,
|
||||
helperDefs,
|
||||
workerMinFunctions,
|
||||
} from "./emit-module.js";
|
||||
import { forkedAnalysis } from "./forked-analysis.js";
|
||||
import {
|
||||
emitLoopCopies,
|
||||
emitSpeculativeWrite,
|
||||
@@ -439,6 +469,12 @@ export interface ForkPrimitives {
|
||||
export interface LlvmTargetOptions {
|
||||
/** C ABI target; omitted direct emission uses the host ABI. */
|
||||
targetTriple?: string;
|
||||
/** Linux (glibc and musl) executables: functions reached only by direct
|
||||
* calls propagate exceptions by unwinding the native stack (unwind-abi.ts). */
|
||||
unwindAbi?: boolean;
|
||||
/** Functions kept on the pending-flag ABI under `unwindAbi`: the ones an
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
@@ -522,15 +558,60 @@ export interface Int32SlotAnalysis {
|
||||
readonly slots: Int32Slots;
|
||||
}
|
||||
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. */
|
||||
function analyzeModuleInt32Slots(mod: IrModule): Int32SlotAnalysis {
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it.
|
||||
* With `revise`, the analysis revises the functions of candidates that drop
|
||||
* (Int32SlotInputs.revise), and `mod` is the module it started from. */
|
||||
function analyzeModuleInt32Slots(
|
||||
mod: IrModule,
|
||||
revise?: (fnName: string, dropped: ReadonlySet<string>) => IrFunction,
|
||||
): Int32SlotAnalysis {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings, nodes: nodeIndex });
|
||||
const slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings,
|
||||
nodes: nodeIndex,
|
||||
...(revise === undefined ? {} : { revise }),
|
||||
});
|
||||
return { mod, nodeIndex, initializerBindings, dynamicStores, slots };
|
||||
}
|
||||
|
||||
/** The candidates an analysis keeps: int32 arrays and checked ones. */
|
||||
function keptInt32Arrays(analysis: Int32SlotAnalysis): ReadonlySet<string> {
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
}
|
||||
|
||||
/** An analysis that revised functions (analyzeModuleInt32Slots' `revise`)
|
||||
* as the analysis of `mod`, the module of the functions it ended with: the
|
||||
* node index of `mod`, and its initializer bindings and dynamic stores,
|
||||
* which must be the ones the analysis read. Null when they differ. */
|
||||
function revisedAnalysis(analysis: Int32SlotAnalysis, mod: IrModule): Int32SlotAnalysis | null {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const read = analysis.initializerBindings;
|
||||
if (initializerBindings.size !== read.size) return null;
|
||||
for (const [name, globals] of initializerBindings) {
|
||||
const other = read.get(name);
|
||||
if (other?.length !== globals.length || globals.some((g, i) => other[i] !== g)) return null;
|
||||
}
|
||||
const stores = analysis.dynamicStores;
|
||||
if (
|
||||
dynamicStores.all !== stores.all ||
|
||||
dynamicStores.names.size !== stores.names.size ||
|
||||
[...dynamicStores.names].some((name) => !stores.names.has(name))
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
mod,
|
||||
nodeIndex,
|
||||
initializerBindings,
|
||||
dynamicStores,
|
||||
slots: analysis.slots,
|
||||
};
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
* result-slot entries (record-results.ts). */
|
||||
function resultEntryTargets(mod: IrModule): ReadonlySet<string> {
|
||||
@@ -545,61 +626,157 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Each round of selectInt32Arrays analyzes the module it marks; the
|
||||
// emitter of the module it returns reuses that round's analysis.
|
||||
const rounds: { last: Int32SlotAnalysis | null } = { last: null };
|
||||
const proven = (marked: IrModule): ReadonlySet<string> => {
|
||||
const analysis = analyzeModuleInt32Slots(marked);
|
||||
rounds.last = analysis;
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
// Each proof selectInt32Arrays asks for analyzes a marked module; the
|
||||
// emitter of the module it returns reuses the last one. A proof that
|
||||
// revised functions analyzed the module of the functions it ended with.
|
||||
const rounds: {
|
||||
last: Int32SlotAnalysis | null;
|
||||
functions: readonly IrFunction[] | null;
|
||||
revising: boolean;
|
||||
} = { last: null, functions: null, revising: true };
|
||||
const proven = (marked: IrModule, revise: Int32ArrayRevision | null): Int32ArrayProof | null => {
|
||||
rounds.functions = null;
|
||||
if (revise === null || !rounds.revising) {
|
||||
rounds.last = analyzeModuleInt32Slots(marked);
|
||||
return { kept: keptInt32Arrays(rounds.last), functions: null };
|
||||
}
|
||||
const latest = new Map<string, IrFunction>();
|
||||
try {
|
||||
rounds.last = analyzeModuleInt32Slots(marked, (fnName, dropped) => {
|
||||
const fn = revise(fnName, dropped);
|
||||
latest.set(fnName, fn);
|
||||
return fn;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof Int32RevisionConflict)) throw error;
|
||||
rounds.last = null;
|
||||
return null;
|
||||
}
|
||||
if (latest.size === 0) return { kept: keptInt32Arrays(rounds.last), functions: null };
|
||||
rounds.functions = marked.functions.map((fn) => latest.get(fn.name) ?? fn);
|
||||
return { kept: keptInt32Arrays(rounds.last), functions: rounds.functions };
|
||||
};
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
const emitted =
|
||||
options.debugSources === undefined
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
if (options.debugSources !== undefined) return new LlEmitter(mod, options, null, null);
|
||||
const passed = versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeLiteralResults(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
pairRowSums(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
proven,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod;
|
||||
const prepared = rounds.last;
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
let emitted = selectInt32Arrays(passed, proven, resultEntryTargets);
|
||||
let prepared = rounds.last;
|
||||
// A proof that revised functions analyzed the module of the functions it
|
||||
// ended with, which is the selected module unless selection fell back to
|
||||
// rounds: then the selected module was proved by its own round.
|
||||
if (prepared !== null && rounds.functions !== null && prepared.mod !== emitted) {
|
||||
const functions = rounds.functions;
|
||||
prepared =
|
||||
emitted !== passed &&
|
||||
emitted.functions.length === functions.length &&
|
||||
emitted.functions.every((fn, i) => fn === functions[i])
|
||||
? revisedAnalysis(prepared, emitted)
|
||||
: null;
|
||||
if (prepared === null && emitted !== passed) {
|
||||
rounds.revising = false;
|
||||
emitted = selectInt32Arrays(passed, proven, resultEntryTargets);
|
||||
prepared = rounds.last;
|
||||
}
|
||||
}
|
||||
if (process.env["SCRIPTC_INT32_VERIFY"] === "1")
|
||||
verifyInt32Selection(passed, emitted, prepared?.mod === emitted ? prepared : null);
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
/** SCRIPTC_INT32_VERIFY=1: throws unless the selection with revisions
|
||||
* (`emitted`, and the analysis the emitter takes over) is the one rounds
|
||||
* without revisions give: the same module, and the same facts. */
|
||||
function verifyInt32Selection(
|
||||
passed: IrModule,
|
||||
emitted: IrModule,
|
||||
analysis: Int32SlotAnalysis | null,
|
||||
): void {
|
||||
let last: Int32SlotAnalysis | null = null;
|
||||
const reference = selectInt32Arrays(
|
||||
passed,
|
||||
(marked) => {
|
||||
last = analyzeModuleInt32Slots(marked);
|
||||
return { kept: keptInt32Arrays(last), functions: null };
|
||||
},
|
||||
resultEntryTargets,
|
||||
);
|
||||
const text = (mod: IrModule): string => serializeModuleParts(mod).join("");
|
||||
if (text(reference) !== text(emitted))
|
||||
throw new InternalCompilerError("int32 selection: the revised module differs from the rounds'");
|
||||
const facts = (mod: IrModule, prepared: Int32SlotAnalysis | null): string =>
|
||||
(prepared?.mod === mod ? prepared : analyzeModuleInt32Slots(mod)).slots.summary(mod);
|
||||
const expected = facts(reference, last);
|
||||
const actual = facts(emitted, analysis);
|
||||
if (expected !== actual) {
|
||||
const want = expected.split("\n");
|
||||
const got = actual.split("\n");
|
||||
const line = want.findIndex((entry, i) => entry !== got[i]);
|
||||
throw new InternalCompilerError(
|
||||
`int32 selection: the revised analysis differs from the rounds': ${want[line]?.slice(0, 400)} != ${got[line]?.slice(0, 400)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits the module's parts, emitting it again with the functions an
|
||||
* UnwindAbiViolation names on the pending-flag ABI. */
|
||||
function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] {
|
||||
for (let excluded = options.unwindExcluded ?? new Set<string>(); ;) {
|
||||
const emitter = moduleEmitter(input, { ...options, unwindExcluded: excluded });
|
||||
try {
|
||||
return emitter.emitParts();
|
||||
} catch (error) {
|
||||
if (!(error instanceof UnwindAbiViolation)) throw error;
|
||||
const names = new Set(excluded);
|
||||
for (const name of emitter.unwindAbi)
|
||||
for (const symbol of [mangleFunction(name), mangleBorrowedFunction(name)])
|
||||
if (error.symbols.has(symbol) || error.symbols.has(symbol + INT32_ABI_SUFFIX))
|
||||
names.add(name);
|
||||
if (names.size === excluded.size) throw error;
|
||||
excluded = names;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
return moduleEmitter(input, options).emit();
|
||||
return emitModuleText(input, options).join("\n");
|
||||
}
|
||||
|
||||
/** emitLlvmModule's text as parts that concatenate to it, without the copy
|
||||
* that joining them makes: a large program's text is 150 MB. */
|
||||
export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
|
||||
return moduleEmitter(input, options)
|
||||
.emitParts()
|
||||
.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
return emitModuleText(input, options).flatMap((part, index) =>
|
||||
index === 0 ? [part] : ["\n", part],
|
||||
);
|
||||
}
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
@@ -609,7 +786,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
// IR validation, contracts and the frontend's disposal run after lowering without checks.
|
||||
relieveHeapPressure(true);
|
||||
const parts = moduleEmitter(input, options).emitParts();
|
||||
const parts = emitModuleText(input, options);
|
||||
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
|
||||
if (length <= 256 * 1024 * 1024) return parts.join("\n");
|
||||
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
@@ -748,8 +925,9 @@ export class LlEmitter {
|
||||
readonly stringSlices = new Map<string, StringSliceSnapshot>();
|
||||
/** Slices written into stack buffers (findScratchStringSlices). */
|
||||
scratchStringSlices: ReadonlySet<IrExpr> = new Set();
|
||||
/** String parameters a caller may lend scratch slices to. */
|
||||
lentStringParameters: LentStringParameters = new Map();
|
||||
/** String parameters, record fields and record producers a caller may
|
||||
* lend scratch slices to. */
|
||||
lendingFacts: LendingFacts = { strings: new Map(), records: new Map(), producers: new Map() };
|
||||
localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
|
||||
localStackObjects = new Map<string, StackObjectSource>();
|
||||
reassignedStackObjects = new Set<string>();
|
||||
@@ -1183,7 +1361,13 @@ export class LlEmitter {
|
||||
readonly checkedNarrows: CheckedNarrows;
|
||||
callArrayReads = new Map<IrExpr, LocalArrayRead>();
|
||||
mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
|
||||
/** Locals of the current function that hold a Map.get result as the
|
||||
* entry's own reference (findMapReadBorrows, borrowableMapGet). */
|
||||
borrowedMapGets: ReadonlySet<string> = new Set();
|
||||
readNumberMapWrites: ReadonlySet<IrExpr> = new Set();
|
||||
/** The integer views of the current function's number Map/Set keys, by
|
||||
* the key's double operand (noteNumberMapKey, map-number-lookup.ts). */
|
||||
numberMapKeyIntegers = new Map<string, LlInteger>();
|
||||
/** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */
|
||||
mapEntryFusion: MapEntryFusion | null = null;
|
||||
readonly callLifetimes: CallLifetimes;
|
||||
@@ -1253,6 +1437,10 @@ export class LlEmitter {
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
/** The functions that propagate exceptions by unwinding (unwind-abi.ts). */
|
||||
readonly unwindAbi: Set<string>;
|
||||
/** Whether the function being emitted is one of unwindAbi. */
|
||||
currentUnwindAbi = false;
|
||||
readonly indirectMayThrow: boolean;
|
||||
/** Worker programs: the functions that poll for termination on entry
|
||||
* (workerTerminationPolls); null outside worker programs. */
|
||||
@@ -1374,7 +1562,12 @@ export class LlEmitter {
|
||||
/** Preorder intervals of the runtime error classes under THIS module's
|
||||
* class-forest numbering (main() stamps scr_error_vts with them, exactly
|
||||
* like the runtime ABI’s errorVtStampLines). */
|
||||
readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = [];
|
||||
readonly errorIntervals: {
|
||||
kind: number;
|
||||
pre: number;
|
||||
post: number;
|
||||
lib: string;
|
||||
}[] = [];
|
||||
/** The runtime emitter vtable's preorder interval, when the program
|
||||
* touches node:events (the class def rides the module exactly then) —
|
||||
* main() stamps scr_emitter_vt with it (emitterVtStampLines, ported). */
|
||||
@@ -1383,7 +1576,12 @@ export class LlEmitter {
|
||||
* ported): the defs ride every emitter-touching module (the frontend
|
||||
* collects the whole emitter-rooted tree), and scr_stream.c links on
|
||||
* the same predicate, so the stamps always have their globals. */
|
||||
readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = [];
|
||||
readonly streamIntervals: {
|
||||
vt: string;
|
||||
pre: number;
|
||||
post: number;
|
||||
lib: string;
|
||||
}[] = [];
|
||||
|
||||
// ── per-function state (reset in emitFunction) ─────────────────────────
|
||||
B = new BlockBuilder();
|
||||
@@ -1419,12 +1617,22 @@ export class LlEmitter {
|
||||
streamingSplitsEnabled = false;
|
||||
readonly initializerBindings: ReturnType<typeof findInitializerBindings>;
|
||||
/** Whole-program int32 storage and parameter/result facts. */
|
||||
readonly int32Slots: Int32Slots;
|
||||
get int32Slots(): Int32Slots {
|
||||
return (this.int32SlotsValue ??= this.joinInt32Slots());
|
||||
}
|
||||
private int32SlotsValue: Int32Slots | undefined;
|
||||
/** The join of the int32 slot analysis a forked process computes
|
||||
* (startInt32Slots). */
|
||||
private int32SlotsJoin: (() => Int32Slots) | undefined;
|
||||
/** The program has no runtime store into a checked-dynamic value
|
||||
* (dynamicFieldStores), so live views of classes and records never
|
||||
* commit back into their objects. Library builds keep their commits:
|
||||
* their values cross a native boundary. */
|
||||
readonly liveViewsReadOnly: boolean;
|
||||
/** The program freezes, seals or prevents extensions of untyped values
|
||||
* (IrModule.untypedIntegrity): the commits of class views store the
|
||||
* view's integrity state in the instance's integrity word. */
|
||||
readonly untypedIntegrity: boolean;
|
||||
/** Functions whose direct calls use i32 parameters or results. */
|
||||
readonly int32Abis: ReadonlyMap<string, Int32Abi>;
|
||||
/** Pure number functions with speculative integer clones. */
|
||||
@@ -1446,7 +1654,12 @@ export class LlEmitter {
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
integerLoopBindings = new Map<
|
||||
string,
|
||||
{ slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } }
|
||||
{
|
||||
slot: string;
|
||||
type: "i32" | "i64";
|
||||
signed?: boolean;
|
||||
range?: { min: number; max: number };
|
||||
}
|
||||
>();
|
||||
/** Constant number locals whose latest emitted declaration stored an
|
||||
* exact integer beside the double, such as `const j = i * 4` in a
|
||||
@@ -1476,11 +1689,50 @@ export class LlEmitter {
|
||||
* integer loop has neither. */
|
||||
bails?: string;
|
||||
ran?: string;
|
||||
/** Writes that speculate a rewritten value (rewriteSpeculable). */
|
||||
values?: ReadonlyMap<IrStmt, SpeculativeValue>;
|
||||
} | null = null;
|
||||
/** Speculative loop sites numbered so far in the module. */
|
||||
speculativeLoopSites = 0;
|
||||
/** The ordinary copy being emitted: resume labels still to place. */
|
||||
speculationResume: Map<IrStmt, { label: string; placed: boolean }> | null = null;
|
||||
/** The rewritten values of the writes `speculationResume` resumes at. */
|
||||
speculationResumeValues: ReadonlyMap<IrStmt, SpeculativeValue> | null = null;
|
||||
/** Hoisted speculative leaves numbered so far in the function being
|
||||
* emitted (emitFunction resets it): a function's local names must not
|
||||
* depend on which functions the same emitter emitted before it, since
|
||||
* function workers each emit a share of the module. */
|
||||
speculativeLeaves = 0;
|
||||
private readonly inlinableFunctions = new Map<string, InlinableFunction | undefined>();
|
||||
|
||||
/** What speculative loop planning may assume here (SpeculationContext). */
|
||||
speculationContext(): SpeculationContext {
|
||||
const inlinable = (callee: string): InlinableFunction | undefined => {
|
||||
if (!this.inlinableFunctions.has(callee)) {
|
||||
// Unset while its body is checked: a recursive helper is not inlinable.
|
||||
this.inlinableFunctions.set(callee, undefined);
|
||||
const fn = this.fnByName.get(callee);
|
||||
this.inlinableFunctions.set(
|
||||
callee,
|
||||
fn ? inlinableNumberFunction(fn, inlinable) : undefined,
|
||||
);
|
||||
}
|
||||
return this.inlinableFunctions.get(callee);
|
||||
};
|
||||
return {
|
||||
inlinable,
|
||||
unaliased: (id) => {
|
||||
const local = this.currentLocals.get(id);
|
||||
return (
|
||||
local !== undefined &&
|
||||
!local.boxed &&
|
||||
!this.captureIds.has(id) &&
|
||||
!this.lazyCaptureBoxes.has(id)
|
||||
);
|
||||
},
|
||||
leafId: () => `%spec.leaf.${this.speculativeLeaves++}`,
|
||||
};
|
||||
}
|
||||
/** Run once at the top of the next loop copy's header
|
||||
* (`enterLoopHeader`): records the ordinary copy's header, or sends the
|
||||
* third copy's header there. */
|
||||
@@ -1490,7 +1742,11 @@ export class LlEmitter {
|
||||
* (the fast copy) or as double arrays (an ordinary copy entered only
|
||||
* after they widened), as `function\0local` keys, and where each of the
|
||||
* fast copy's writes resumes in the third copy. */
|
||||
int32Kinds: { int32: Set<string>; wide: Set<string>; resume: Map<IrStmt, string> } | null = null;
|
||||
int32Kinds: {
|
||||
int32: Set<string>;
|
||||
wide: Set<string>;
|
||||
resume: Map<IrStmt, string>;
|
||||
} | null = null;
|
||||
/** The resume label of the fast-copy write being emitted. */
|
||||
int32KindResume: string | null = null;
|
||||
/** The function being emitted, for speculative globals. */
|
||||
@@ -1518,9 +1774,23 @@ export class LlEmitter {
|
||||
* (one-unit tests and switches, charAtUnitRead), with the charAt each
|
||||
* stands for: a loop version's proof for the charAt covers its read. */
|
||||
readonly charAtUnitReads = new Map<IrExpr, IrExpr>();
|
||||
/** String reads a loop version proved in bounds of a string's UTF-16
|
||||
* units, with the units pointer to read (string-unit-versions.ts). */
|
||||
stringUnitReads: ReadonlyMap<IrExpr | IrStmt, string> = new Map();
|
||||
/** `length` reads of a loop version's string, with the i64 UTF-16
|
||||
* length its guard took (bytes-loop-versions.ts). */
|
||||
stringLengthReads: ReadonlyMap<IrExpr, string> = new Map();
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
/** The alias-scope attachment of each typed-array element read and write
|
||||
* in the current loop version's fast copy (emitVersionedLoop), and the
|
||||
* one the element access being emitted carries (bytesElementAlias). */
|
||||
bytesAliasScopes: ReadonlyMap<IrExpr | IrStmt, string> = new Map();
|
||||
bytesAccessScope = "";
|
||||
/** Loop versions with alias scopes in the current function; it names
|
||||
* their scopes. */
|
||||
aliasScopedVersions = 0;
|
||||
integerViews = new Map<string, string>();
|
||||
/** Module globals the current function owns outright (initializer
|
||||
* bindings): their function-local slot, which stands in for the global. */
|
||||
@@ -1550,7 +1820,12 @@ export class LlEmitter {
|
||||
* releases frames/scopes down to the recorded depths and branches to
|
||||
* `label` (the catch, or the exception-path finally) instead of
|
||||
* returning out of the function. Entering a try emits no code. */
|
||||
tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = [];
|
||||
tryStack: {
|
||||
label: string;
|
||||
used: boolean;
|
||||
frameDepth: number;
|
||||
scopeDepth: number;
|
||||
}[] = [];
|
||||
/** Return type of the function being emitted — the unwind path returns
|
||||
* a dummy of this type (never read: callers check the flag first). */
|
||||
currentReturnType: IrType = VOID;
|
||||
@@ -1615,6 +1890,15 @@ export class LlEmitter {
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
// Started before the other analyses, which may run while a forked
|
||||
// process computes it (startInt32Slots).
|
||||
if (int32Analysis !== null) this.int32SlotsValue = int32Analysis.slots;
|
||||
else this.startInt32Slots(dynamicStores);
|
||||
this.stackTraces = observesStacks(mod, this.nodeIndex);
|
||||
this.nullableUnions = new NullableUnions(
|
||||
mod,
|
||||
@@ -1633,20 +1917,12 @@ export class LlEmitter {
|
||||
this.nullableUnions.armTypes(),
|
||||
);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots =
|
||||
int32Analysis?.slots ??
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.untypedIntegrity = mod.untypedIntegrity === true;
|
||||
this.liveViewsReadOnly =
|
||||
mod.lib === undefined && !dynamicStores.all && dynamicStores.names.size === 0;
|
||||
mod.lib === undefined &&
|
||||
!this.untypedIntegrity &&
|
||||
!dynamicStores.all &&
|
||||
dynamicStores.names.size === 0;
|
||||
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
|
||||
this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
|
||||
this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
|
||||
@@ -1714,6 +1990,16 @@ export class LlEmitter {
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.mayThrow = mt.fns;
|
||||
this.unwindAbi =
|
||||
options.unwindAbi === true &&
|
||||
!this.wasi &&
|
||||
mod.lib === undefined &&
|
||||
options.debugSources === undefined &&
|
||||
(options.targetTriple
|
||||
? /-linux-(?:gnu|musl)/.test(options.targetTriple)
|
||||
: process.platform === "linux")
|
||||
? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded)
|
||||
: new Set();
|
||||
this.workerEntryPolls = mt.workerEntryPolls ?? null;
|
||||
this.stackChecks = mt.stackChecks ?? null;
|
||||
// A stackless call's eager prefix borrows its caller's stack; the
|
||||
@@ -1829,8 +2115,14 @@ export class LlEmitter {
|
||||
},
|
||||
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
if (this.debug === null) {
|
||||
const strings = findLentStringParameters(this.fnByName, this.callLifetimes);
|
||||
this.lendingFacts = {
|
||||
strings,
|
||||
records: findLentRecordFields(this.fnByName, this.callLifetimes, strings),
|
||||
producers: findRecordFieldProducers(this.fnByName),
|
||||
};
|
||||
}
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes, this.nodeIndex);
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
@@ -1849,6 +2141,9 @@ export class LlEmitter {
|
||||
: undefined,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
// Integer clones and their dispatchers call each other through their
|
||||
// own lowering, which keeps plain calls and flag tests.
|
||||
for (const name of this.integerClones.members) this.unwindAbi.delete(name);
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
@@ -1941,14 +2236,14 @@ export class LlEmitter {
|
||||
const meta = this.classMeta.get(t.className);
|
||||
return meta !== undefined && programLifetimeClass(this.shapeHost, meta);
|
||||
},
|
||||
classSubtypes: (name) => {
|
||||
const target = this.classMetaOf(name);
|
||||
const intervals = classMembershipIntervals(this.classMeta, target.def.name);
|
||||
return [...this.classMeta.values()]
|
||||
.filter((meta) =>
|
||||
intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post),
|
||||
)
|
||||
.map((meta) => meta.def.name);
|
||||
classSubtypes: (name) => this.classSubtypes(name),
|
||||
classValueSubtypes: (name) => {
|
||||
const target = this.classMetaOf(name).def;
|
||||
return this.classSubtypes(name).filter(
|
||||
(sub) =>
|
||||
sub !== target.name &&
|
||||
classValueAbiEquals(this.classMetaOf(sub).def, target, this.fnByName),
|
||||
);
|
||||
},
|
||||
pendingTestLines: (dest) => this.pendingTestLines(dest),
|
||||
virtualEntry: (implFn) => this.virtualEntry(implFn),
|
||||
@@ -2067,6 +2362,46 @@ export class LlEmitter {
|
||||
);
|
||||
// After the vtable slots: borrowing slots exclude their implementations.
|
||||
if (this.debug === null) this.analyzeBorrowedReturns();
|
||||
this.int32SlotsValue ??= this.joinInt32Slots();
|
||||
}
|
||||
|
||||
/** Computes the int32 slot analysis, or starts it on a forked process.
|
||||
* Dev builds read its result only near the end of the constructor
|
||||
* (int32Abis is empty with debug info), so for a large module the
|
||||
* process computes it while the constructor runs the other analyses. */
|
||||
private startInt32Slots(dynamicStores: DynamicFieldStores): void {
|
||||
const mod = this.mod;
|
||||
const nodes = this.nodeIndex;
|
||||
const analyze = (): Int32Slots =>
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes,
|
||||
});
|
||||
const fork = this.targetOptions.forkPrimitives;
|
||||
if (
|
||||
fork === undefined ||
|
||||
this.targetOptions.debugSources === undefined ||
|
||||
(this.targetOptions.functionWorkers ?? 1) < 2 ||
|
||||
mod.functions.length < workerMinFunctions()
|
||||
) {
|
||||
this.int32SlotsValue = analyze();
|
||||
return;
|
||||
}
|
||||
this.int32SlotsJoin = forkedAnalysis(
|
||||
fork,
|
||||
analyze,
|
||||
(slots) => slots.encode(nodes),
|
||||
(text) => Int32Slots.decode(text, mod, nodes),
|
||||
);
|
||||
}
|
||||
|
||||
private joinInt32Slots(): Int32Slots {
|
||||
const join = this.int32SlotsJoin;
|
||||
if (join === undefined)
|
||||
throw new InternalCompilerError("llvm emitter bug: int32 slots read before analysis");
|
||||
this.int32SlotsJoin = undefined;
|
||||
return join();
|
||||
}
|
||||
|
||||
/** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */
|
||||
@@ -2128,7 +2463,8 @@ export class LlEmitter {
|
||||
return this.emitParts().join("\n");
|
||||
}
|
||||
emitParts(): string[] {
|
||||
return emitParts(this);
|
||||
const parts = emitParts(this);
|
||||
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
@@ -3238,6 +3574,13 @@ export class LlEmitter {
|
||||
terminator = `br label %${target.label}`;
|
||||
} else if (this.currentCoro !== null) {
|
||||
terminator = `br label %${this.currentCoro.finalLabel}`;
|
||||
} else if (this.currentUnwindAbi && this.inLandingPad) {
|
||||
// The unwind that entered the landing pad goes on to the caller's.
|
||||
terminator = UNWIND_RESUME;
|
||||
} else if (this.currentUnwindAbi) {
|
||||
// The caller's invoke takes the exception at its landing pad.
|
||||
this.declare(UNWIND_RAISE_DECLARATION);
|
||||
terminator = UNWIND_RAISE;
|
||||
} else {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
terminator =
|
||||
@@ -3268,6 +3611,7 @@ export class LlEmitter {
|
||||
passThrough !== undefined &&
|
||||
!target &&
|
||||
this.currentCoro === null &&
|
||||
!this.currentUnwindAbi &&
|
||||
!this.currentInt32Result
|
||||
) {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
@@ -3454,6 +3798,46 @@ export class LlEmitter {
|
||||
B.startBlock(B.newLabel("exc.dead"));
|
||||
}
|
||||
|
||||
/** Emits `line`, a `call` of `callee`, and reports whether the call
|
||||
* needs no pending check after it: a call of an unwinding-ABI function
|
||||
* becomes an `invoke` whose landing pad runs this frame's unwind (the
|
||||
* same releases and handler as emitPendingCheck's unwind path), and the
|
||||
* normal path continues in a fresh block. Coroutine bodies keep plain
|
||||
* calls (unwindAbiFunctions excludes their callees). */
|
||||
emitDirectCall(line: string, callee: string): boolean {
|
||||
const B = this.B;
|
||||
if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) {
|
||||
B.line(line);
|
||||
return false;
|
||||
}
|
||||
const ok = B.newLabel("inv.ok");
|
||||
const pad = B.newLabel("inv.lp");
|
||||
this.usesLandingPads = true;
|
||||
// A trailing comment goes after the invoke's labels.
|
||||
const comment = line.indexOf(" ; ");
|
||||
const text = comment < 0 ? line : line.slice(0, comment);
|
||||
B.terminate(
|
||||
`${text.replace(/^((?:%[\w.]+ = )?)call /, "$1invoke ")} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
if (this.currentUnwindAbi) B.line(`store { ptr, i32 } ${exception}, ptr ${UNWIND_SLOT}`);
|
||||
this.inLandingPad = true;
|
||||
this.emitUnwind();
|
||||
this.inLandingPad = false;
|
||||
B.startBlock(ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Whether emitUnwind runs for a landing pad, whose unwind goes on with
|
||||
* `resume` instead of raising again. */
|
||||
private inLandingPad = false;
|
||||
|
||||
/** Whether the function being emitted has a landing pad (its definition
|
||||
* needs the personality). */
|
||||
usesLandingPads = false;
|
||||
|
||||
/** A recursive function's entry check: below the running stack's guard,
|
||||
* throw Node's RangeError and unwind like any other pending exception. */
|
||||
emitStackCheck(): void {
|
||||
@@ -4024,6 +4408,15 @@ export class LlEmitter {
|
||||
|
||||
// ── class plumbing ──────────────────────────────────────────────────────
|
||||
|
||||
/** `name` and every class whose membership interval falls in its own. */
|
||||
private classSubtypes(name: string): string[] {
|
||||
const target = this.classMetaOf(name);
|
||||
const intervals = classMembershipIntervals(this.classMeta, target.def.name);
|
||||
return [...this.classMeta.values()]
|
||||
.filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post))
|
||||
.map((meta) => meta.def.name);
|
||||
}
|
||||
|
||||
classMetaOf(className: string): LlClassMeta {
|
||||
const meta = this.classMeta.get(className);
|
||||
if (!meta) throw new InternalCompilerError(`llvm emitter bug: unknown class ${className}`);
|
||||
@@ -4370,11 +4763,21 @@ export class LlEmitter {
|
||||
workers: this.mod.workers === true,
|
||||
owned: this.bytesOwned,
|
||||
scalarStrings: new Set(this.stringSlices.keys()),
|
||||
unitSafeFunctions: this.unitSafeFunctions(),
|
||||
},
|
||||
paired,
|
||||
);
|
||||
}
|
||||
|
||||
private unitSafe: ReadonlySet<string> | null | undefined;
|
||||
/** The functions a loop over a string's UTF-16 units may call, or null
|
||||
* where emitted reads do not use the runtime's scan receiver. */
|
||||
private unitSafeFunctions(): ReadonlySet<string> | null {
|
||||
if (this.unitSafe === undefined)
|
||||
this.unitSafe = stringScanInline(this) ? unitSafeFunctions(this.mod) : null;
|
||||
return this.unitSafe;
|
||||
}
|
||||
|
||||
/** The alias tag of a whole-slot load of a binding (module globals only). */
|
||||
slotAlias(id: string): string {
|
||||
const binding = this.binding(id);
|
||||
@@ -5217,7 +5620,22 @@ export class LlEmitter {
|
||||
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
|
||||
const ret = this.llType(fn.returnType);
|
||||
const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
|
||||
B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// The owned entry of an unwinding-ABI body releases the borrowed
|
||||
// parameters on the unwind too, then resumes it to its caller.
|
||||
const unwinding = this.unwindAbi.has(fn.name) && kept.size === 0;
|
||||
if (unwinding) {
|
||||
const ok = B.newLabel("adapt.ok");
|
||||
const pad = B.newLabel("adapt.lp");
|
||||
B.terminate(
|
||||
`${ret === "void" ? "" : "%result = "}invoke ${call.slice(5)} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type);
|
||||
B.terminate(`resume { ptr, i32 } ${exception}`);
|
||||
B.startBlock(ok);
|
||||
} else B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// A borrowed result is reachable from the parameters: own it first. A
|
||||
// throwing body returns a null dummy, which needs no owner (and string
|
||||
// or array retains do not accept NULL).
|
||||
@@ -5243,7 +5661,9 @@ export class LlEmitter {
|
||||
B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
|
||||
const symbol =
|
||||
kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
|
||||
if (unwinding) this.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
const personality = unwinding ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS}${personality} {\n${B.render()}\n}`;
|
||||
}
|
||||
canBorrowCallArgument(value: IrExpr): boolean {
|
||||
return canBorrowCallArgument(this, value);
|
||||
@@ -5656,6 +6076,22 @@ export class LlEmitter {
|
||||
if (resume && !resume.placed && !this.B.isTerminated()) {
|
||||
// The fast copy's failed checks re-execute this statement here.
|
||||
resume.placed = true;
|
||||
// A write with hoisted leaves resumed from its own failed check
|
||||
// re-executes its arithmetic over the leaves' stored doubles: each
|
||||
// leaf ran once, in the fast copy. Reached from an earlier statement
|
||||
// of this copy (a failed check before it), its leaves never ran, so
|
||||
// it runs as written.
|
||||
const rewritten = this.speculationResumeValues?.get(s);
|
||||
if (rewritten !== undefined && rewritten.leaves.length > 0) {
|
||||
const join = this.B.newLabel("spec.resume.join");
|
||||
this.emitStmt(s);
|
||||
if (!this.B.isTerminated()) this.B.br(join);
|
||||
this.B.startBlock(resume.label);
|
||||
this.emitStmt(withSpeculativeValue(s, rewritten.value));
|
||||
if (!this.B.isTerminated()) this.B.br(join);
|
||||
this.B.startBlock(join);
|
||||
return;
|
||||
}
|
||||
this.B.br(resume.label);
|
||||
this.B.startBlock(resume.label);
|
||||
}
|
||||
@@ -6905,7 +7341,10 @@ export class LlEmitter {
|
||||
return childDataDynThunkFor(this, chunk);
|
||||
}
|
||||
|
||||
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
|
||||
emitterFixedAdapter(cbT: IrType & { kind: "func" }): {
|
||||
fn: string;
|
||||
shim: string;
|
||||
} {
|
||||
return emitterFixedAdapter(this, cbT);
|
||||
}
|
||||
|
||||
@@ -7052,8 +7491,9 @@ export class LlEmitter {
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): LlValue {
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned);
|
||||
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned, scope);
|
||||
}
|
||||
|
||||
emitToUint32(value: string, expr?: IrExpr, uint32?: string): string {
|
||||
@@ -7117,8 +7557,20 @@ export class LlEmitter {
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): void {
|
||||
return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned);
|
||||
return emitBytesSet(
|
||||
this,
|
||||
elem,
|
||||
receiver,
|
||||
index,
|
||||
value,
|
||||
indexExpr,
|
||||
expr,
|
||||
inBounds,
|
||||
owned,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
|
||||
emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue {
|
||||
|
||||
@@ -235,6 +235,26 @@ export function emitBytesGet(
|
||||
inBounds = false,
|
||||
invalidNaN = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): LlValue {
|
||||
const outer = host.bytesAccessScope;
|
||||
host.bytesAccessScope = scope;
|
||||
try {
|
||||
return emitScopedBytesGet(host, elem, receiver, index, expr, inBounds, invalidNaN, owned);
|
||||
} finally {
|
||||
host.bytesAccessScope = outer;
|
||||
}
|
||||
}
|
||||
|
||||
function emitScopedBytesGet(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
index: LlValue,
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
invalidNaN: boolean,
|
||||
owned: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
// Any invalid index traps (the array runtime's discipline).
|
||||
@@ -547,6 +567,27 @@ export function emitBytesSet(
|
||||
expr?: IrExpr,
|
||||
inBounds = false,
|
||||
owned = false,
|
||||
scope = "",
|
||||
): void {
|
||||
const outer = host.bytesAccessScope;
|
||||
host.bytesAccessScope = scope;
|
||||
try {
|
||||
emitScopedBytesSet(host, elem, receiver, index, value, indexExpr, expr, inBounds, owned);
|
||||
} finally {
|
||||
host.bytesAccessScope = outer;
|
||||
}
|
||||
}
|
||||
|
||||
function emitScopedBytesSet(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
index: LlValue,
|
||||
value: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
expr: IrExpr | undefined,
|
||||
inBounds: boolean,
|
||||
owned: boolean,
|
||||
): void {
|
||||
const B = host.B;
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
@@ -768,6 +809,7 @@ export function emitBytesIntrinsic(
|
||||
host.bytesBounds.has(e),
|
||||
e.invalidNaN === true,
|
||||
host.bytesOwned.has(e),
|
||||
host.bytesAliasScopes.get(e),
|
||||
);
|
||||
case "slice":
|
||||
return call(
|
||||
|
||||
@@ -244,38 +244,46 @@ export function emitCallExpr(
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`;
|
||||
// A call of an unwinding-ABI callee is an invoke (emitDirectCall):
|
||||
// its landing pad unwinds, so no pending check follows it.
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (abi?.union) {
|
||||
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
if (stackResult) return host.stackScalarUnion(t, e.type);
|
||||
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
|
||||
}
|
||||
if (abi?.result) {
|
||||
B.line(`${t} = call i32 ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee);
|
||||
const out = host.int32Result(t, e.type);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
let out: LlValue;
|
||||
if (borrowed && host.borrowedReturns.has(e.callee)) {
|
||||
// A borrowed-return body hands back +0 (a null dummy when it threw):
|
||||
// test the exception first, then own the result before the
|
||||
// argument snapshots that keep it reachable are released.
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
out = host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
} else {
|
||||
out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
}
|
||||
if (borrowed) {
|
||||
host.toFrame(out);
|
||||
@@ -1366,10 +1374,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
|
||||
)
|
||||
.join(", ");
|
||||
const t = host.B.tmp();
|
||||
host.B.line(
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`,
|
||||
e.callee,
|
||||
);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
emitStringMapGetRef,
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapHas,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
emitUnitCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -322,6 +324,15 @@ export function charAtUnitRead(
|
||||
return read;
|
||||
}
|
||||
|
||||
/** The UTF-16 units an enclosing loop version proved the index of `e`
|
||||
* below the count of (string-unit-versions.ts). */
|
||||
function provenUnitsRead(host: LlvmEmitterContext, e: IrExpr): string | null {
|
||||
const units = host.stringUnitReads.get(e);
|
||||
if (units !== undefined) return units;
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined ? (host.stringUnitReads.get(origin) ?? null) : null;
|
||||
}
|
||||
|
||||
/** An enclosing loop version proved the string of `e` ASCII and its index
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
@@ -364,9 +375,14 @@ export function emitStrIntrinsic(
|
||||
const method = e.method;
|
||||
switch (method) {
|
||||
case "length": {
|
||||
const t = B.tmp();
|
||||
const length = host.stringLengthReads.get(e);
|
||||
if (length !== undefined) {
|
||||
B.line(`${t} = sitofp i64 ${length} to double`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// sc_str_utf16_len (string-reads.ts) answers proven-ASCII strings inline.
|
||||
host.declare(STRING_UTF16_LEN_DECL);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call double @sc_str_utf16_len(ptr ${r.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -376,6 +392,8 @@ export function emitStrIntrinsic(
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
const wide = widenInteger(host, index);
|
||||
const units = provenUnitsRead(host, e);
|
||||
if (units !== null) return { name: emitUnitCharCode(host, units, wide), type: e.type };
|
||||
if (provenStringRead(host, e))
|
||||
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
@@ -1127,6 +1145,65 @@ export function emitArrIntrinsic(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `e` is a Map.get that emitBorrowedMapGet can emit without a
|
||||
* retain: a typed map with number or inline string keys, a reference value,
|
||||
* and a nullable result (the value's pointer, or the absent arm's). */
|
||||
export function borrowableMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== "get" || e.args.length !== 1) return false;
|
||||
if (!borrowsMapReadInputs(e) || e.receiver.type.kind !== "map") return false;
|
||||
const { key, value } = e.receiver.type;
|
||||
if (!isRefCounted(value) || value.kind === "union" || key.kind === "union") return false;
|
||||
if (e.type.kind !== "union" || !host.nullableUnions.has(e.type.unionId)) return false;
|
||||
const arg = e.args[0]!;
|
||||
const access = mapKeyAccess(key);
|
||||
return (
|
||||
(access === "f64" && arg.type.kind === "f64") ||
|
||||
(access === "str" && arg.type.kind === "string")
|
||||
);
|
||||
}
|
||||
|
||||
/** A borrowableMapGet read as the entry's own reference: no retain, and
|
||||
* no owner. The caller proves that nothing can change the map while the
|
||||
* value is used (findMapReadBorrows). Null, before emitting anything, for
|
||||
* a key held as a split span, which takes the runtime's owned lookup. */
|
||||
export function emitBorrowedMapGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "mapIntrinsic" },
|
||||
): LlValue | null {
|
||||
const arg = e.args[0]!;
|
||||
if (arg.kind === "varRef" && host.splitSpans.get(arg.localId) !== undefined) return null;
|
||||
const B = host.B;
|
||||
const mapType = e.receiver.type as IrType & { kind: "map" };
|
||||
const r = host.emitStableReceiver(e.receiver, e.args);
|
||||
const k = emitMapLookupKey(host, e.args[0]!, true);
|
||||
const numberKey = inlineNumberKey(k);
|
||||
const stringKey = entryStringKey(k);
|
||||
const value = mapType.value;
|
||||
let raw: string;
|
||||
if (numberKey !== null) raw = emitNumberMapGetRef(host, r.name, numberKey, value, true);
|
||||
else if (stringKey !== null)
|
||||
raw = emitStringMapGetRef(
|
||||
host,
|
||||
r.name,
|
||||
stringKey,
|
||||
value,
|
||||
compactMapValues(host, mapType.key, value),
|
||||
true,
|
||||
);
|
||||
else throw new InternalCompilerError("llvm emitter bug: borrowed Map.get without an inline key");
|
||||
const resultType = e.type as IrType & { kind: "union" };
|
||||
const absent = host.unitInstanceRef(
|
||||
resultType.unionId,
|
||||
undefinedArmTag(resultType, host.unionsById),
|
||||
);
|
||||
if (absent === "null") return { name: raw, type: e.type };
|
||||
const isnull = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
export function wrapNullable(
|
||||
host: LlvmEmitterContext,
|
||||
raw: string,
|
||||
@@ -1202,7 +1279,9 @@ function collectionOps(host: LlvmEmitterContext, key: IrType, value: IrType): st
|
||||
function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): string {
|
||||
const B = host.B;
|
||||
const arms = key.kind === "union" ? host.unionsById.get(key.unionId)?.arms : undefined;
|
||||
const kind = mapKeyKindNum(key, arms);
|
||||
// SCR_MAP_KEY_REF (2): shape-union keys are record pointers.
|
||||
const kind =
|
||||
key.kind === "union" && host.nullableUnions.shapes(key.unionId) ? 2 : mapKeyKindNum(key, arms);
|
||||
const m = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`);
|
||||
// SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues).
|
||||
@@ -1529,6 +1608,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 +1635,7 @@ export function emitMapLikeIntrinsic(
|
||||
// Element borrowed (the runtime retains stored strings); the unit
|
||||
// value is 0. Re-adding overwrites in place, preserving insertion.
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!);
|
||||
if (kAcc === "f64" && host.readNumberMapWrites.has(e))
|
||||
emitNumberMapSetScalar(host, r.name, k.name, null, () =>
|
||||
host.mapSet(r.name, kAcc, "f64", k.name, f64Lit(0)),
|
||||
@@ -1564,7 +1645,10 @@ export function emitMapLikeIntrinsic(
|
||||
}
|
||||
case "has": {
|
||||
const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
if (kAcc === "f64") {
|
||||
noteNumberMapKey(host, k, e.args[0]!);
|
||||
return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
}
|
||||
if (kAcc === "str")
|
||||
return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type };
|
||||
host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`);
|
||||
|
||||
@@ -30,6 +30,9 @@ export interface WideInteger {
|
||||
name: string;
|
||||
below: number;
|
||||
bound: number;
|
||||
/** The double is the integer rounded once: the result of one operation on
|
||||
* operands that always equal their doubles. */
|
||||
single?: true;
|
||||
}
|
||||
|
||||
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DYN_KIND } from "./dyn.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
|
||||
import { emitUnionTag } from "./union-repr.js";
|
||||
|
||||
export function emitControlExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -52,6 +54,8 @@ export function emitControlExpr(
|
||||
case "toBool":
|
||||
return { name: host.truthy(host.emitUnionProjection(e.operand)), type: e.type };
|
||||
case "logical": {
|
||||
const tagSet = e.op === "||" ? unionTagSet(e) : null;
|
||||
if (tagSet) return { name: emitUnionTagSet(host, tagSet), type: e.type };
|
||||
// JS value semantics: the result is the deciding operand itself.
|
||||
// Left evaluates once, ownership moves into the result slot; when
|
||||
// the branch takes the right operand the stale left releases first
|
||||
@@ -614,3 +618,85 @@ function literalChoice(e: IrExpr): boolean {
|
||||
if (e.kind === "strLit") return true;
|
||||
return e.kind === "ternary" && literalChoice(e.then) && literalChoice(e.else_);
|
||||
}
|
||||
|
||||
type TagSet = { unionId: string; value: IrExpr; tags: Set<number> };
|
||||
|
||||
/** An `||` chain of two or more tag tests of one local (`isTag(u, 1) ||
|
||||
* isTag(u, 4) || ...`, the shape of a union conversion's identity test). */
|
||||
function unionTagSet(e: IrExpr & { kind: "logical" }): TagSet | null {
|
||||
const leaves: IrExpr[] = [];
|
||||
const collect = (x: IrExpr): void => {
|
||||
if (x.kind === "logical" && x.op === "||") {
|
||||
collect(x.left);
|
||||
collect(x.right);
|
||||
} else leaves.push(x);
|
||||
};
|
||||
collect(e);
|
||||
const first = leaves[0]!;
|
||||
if (first.kind !== "unionIsTag" || first.value.kind !== "varRef") return null;
|
||||
const localId = first.value.localId;
|
||||
const tags = new Set<number>();
|
||||
for (const leaf of leaves) {
|
||||
if (
|
||||
leaf.kind !== "unionIsTag" ||
|
||||
leaf.negated ||
|
||||
leaf.unionId !== first.unionId ||
|
||||
leaf.value.kind !== "varRef" ||
|
||||
leaf.value.localId !== localId
|
||||
)
|
||||
return null;
|
||||
tags.add(leaf.tag);
|
||||
}
|
||||
return { unionId: first.unionId, value: first.value, tags };
|
||||
}
|
||||
|
||||
/** i1: the union value holds one of the tags. One read of the tag and one
|
||||
* switch over the set; a shape union switches on the record's shape word
|
||||
* after its unit encodings. */
|
||||
function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string {
|
||||
const B = host.B;
|
||||
const u = host.emitUnionProjection(set.value).name;
|
||||
const nullable = host.nullableUnions.get(set.unionId);
|
||||
const yes = B.newLabel("tags.y");
|
||||
const no = B.newLabel("tags.n");
|
||||
const join = B.newLabel("tags.j");
|
||||
const target = (tag: number): string => (set.tags.has(tag) ? yes : no);
|
||||
if (nullable?.repr === "shapes") {
|
||||
if (nullable.unitTag >= 0) {
|
||||
const isNull = B.tmp();
|
||||
const next = B.newLabel("tags.r");
|
||||
B.line(`${isNull} = icmp eq ptr ${u}, null`);
|
||||
B.condBr(isNull, target(nullable.unitTag), next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
if (nullable.nullTag >= 0) {
|
||||
const isNullArm = B.tmp();
|
||||
const next = B.newLabel("tags.r");
|
||||
B.line(`${isNullArm} = icmp eq ptr ${u}, ${NULLABLE_NULL}`);
|
||||
B.condBr(isNullArm, target(nullable.nullTag), next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
const wp = B.tmp();
|
||||
const w = B.tmp();
|
||||
B.line(
|
||||
`${wp} = getelementptr inbounds ${shapeWordHeader(host.sizeType)}, ptr ${u}, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
);
|
||||
B.line(`${w} = load i32, ptr ${wp}`);
|
||||
const cases = nullable.arms
|
||||
.filter((arm) => set.tags.has(arm.tag))
|
||||
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %${yes}`);
|
||||
B.terminate(`switch i32 ${w}, label %${no} [ ${cases.join(" ")} ]`);
|
||||
} else {
|
||||
const tag = nullable ? emitUnionTag(B, nullable, u) : host.unionTag(u, set.unionId);
|
||||
const cases = [...set.tags].map((t) => `i32 ${t}, label %${yes}`);
|
||||
B.terminate(`switch i32 ${tag}, label %${no} [ ${cases.join(" ")} ]`);
|
||||
}
|
||||
B.startBlock(yes);
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`);
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,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 +121,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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
@@ -438,10 +438,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 +482,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 +499,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 +511,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 +529,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}`;
|
||||
@@ -843,8 +867,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 +1003,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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*
|
||||
* 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)
|
||||
@@ -384,6 +385,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 +403,10 @@ 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.
|
||||
host.declare(`declare ptr @scr_js_get_probe(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 @scr_js_get_probe(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 +634,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 +748,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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import { F64, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { BYTES_ELEMENT_SIZE, F64, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitJsLibCall } from "./jsval.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
} from "./lib-shared.js";
|
||||
import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { caBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */
|
||||
const TYPE_KEY_LEN_MASK = (1 << 27) - 1;
|
||||
@@ -595,7 +596,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// The https URL row is the http one with the TLS entry point — same
|
||||
// three arguments, same response-callback adapter. The https options
|
||||
// row is wider: rejectUnauthorized stays an i1, while its ScrStr or
|
||||
// ScrBytes CA value expands to the runtime's raw pointer + length.
|
||||
// ScrBytes CA value expands to the runtime's raw pointer + length
|
||||
// (caBytes: a null pointer for none).
|
||||
const isTls = e.fn.startsWith("https.");
|
||||
const isUrl = e.fn.includes("requestUrl");
|
||||
const isTlsOptions = isTls && !isUrl;
|
||||
@@ -629,7 +631,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (isTlsOptions) {
|
||||
callArgs = [
|
||||
...callArgs.slice(0, 8),
|
||||
...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
|
||||
...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
|
||||
];
|
||||
host.declare(
|
||||
`declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
@@ -649,6 +651,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
|
||||
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
}
|
||||
// A setter's write: the time value into the Date object (ScrDate.ms).
|
||||
if (e.fn === "date.store") {
|
||||
const date = emitBorrowedInput(host, e.args[0]!).name;
|
||||
const ms = host.emitExpr(e.args[1]!).name;
|
||||
const field = B.tmp();
|
||||
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
|
||||
B.line(`store double ${ms}, ptr ${field}`);
|
||||
return { name: ms, type: e.type };
|
||||
}
|
||||
// queueMicrotask's checked-dynamic form carries the AsyncLocalStorage
|
||||
// context like the static one (lib-process.ts).
|
||||
const sym =
|
||||
@@ -927,6 +938,57 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type });
|
||||
if (e.fn === "bytes.disjoint") {
|
||||
// [data, data + len * size) of each view, compared as addresses. A view
|
||||
// that is not there (a binding the nest never assigned, NULL) answers
|
||||
// false without reading its header, so the caller keeps its loop.
|
||||
const B = host.B;
|
||||
const values = e.args.map((arg) => ({ arg, value: host.emitReadReceiver(arg) }));
|
||||
const present = values.map(({ value }) => {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = icmp ne ptr ${value.name}, null`);
|
||||
return t;
|
||||
});
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${present[0]}, ${present[1]}`);
|
||||
const compare = B.newLabel("disjoint.compare"),
|
||||
done = B.newLabel("disjoint.done");
|
||||
const from = B.currentLabel();
|
||||
B.condBr(both, compare, done);
|
||||
B.startBlock(compare);
|
||||
const range = (arg: IrExpr, pointer: string): { start: string; end: string } => {
|
||||
const size = arg.type.kind === "bytes" ? BYTES_ELEMENT_SIZE[arg.type.elem] : 1;
|
||||
const dataSlot = B.tmp(),
|
||||
data = B.tmp(),
|
||||
lenSlot = B.tmp(),
|
||||
len = B.tmp(),
|
||||
bytes = B.tmp(),
|
||||
start = B.tmp(),
|
||||
end = B.tmp();
|
||||
B.line(`${dataSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 3`);
|
||||
B.line(`${data} = load ptr, ptr ${dataSlot}`);
|
||||
B.line(`${lenSlot} = getelementptr inbounds %ScrBytes, ptr ${pointer}, i64 0, i32 1`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${lenSlot}`);
|
||||
B.line(`${bytes} = mul ${host.sizeType} ${len}, ${size}`);
|
||||
B.line(`${start} = ptrtoint ptr ${data} to ${host.sizeType}`);
|
||||
B.line(`${end} = add ${host.sizeType} ${start}, ${bytes}`);
|
||||
return { start, end };
|
||||
};
|
||||
const left = range(values[0]!.arg, values[0]!.value.name),
|
||||
right = range(values[1]!.arg, values[1]!.value.name);
|
||||
const before = B.tmp(),
|
||||
after = B.tmp(),
|
||||
apart = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${before} = icmp ule ${host.sizeType} ${left.end}, ${right.start}`);
|
||||
B.line(`${after} = icmp ule ${host.sizeType} ${right.end}, ${left.start}`);
|
||||
B.line(`${apart} = or i1 ${before}, ${after}`);
|
||||
const compared = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
B.line(`${t} = phi i1 [ false, %${from} ], [ ${apart}, %${compared} ]`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "record.isFrozen") {
|
||||
// The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is
|
||||
// its second-highest bit; an immortal word sets the highest one too.
|
||||
@@ -1069,6 +1131,36 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
}
|
||||
}
|
||||
|
||||
/** The settlement adapter of Promise.all's tuple form for the builder
|
||||
* `fnName`, which fulfills the result with the builder's tuple. */
|
||||
function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrType): string {
|
||||
const key = `allTuple:${fnName}`;
|
||||
let adapter = host.resolveThunks.get(key);
|
||||
if (adapter !== undefined) return adapter;
|
||||
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, adapter);
|
||||
host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]);
|
||||
const rc = vAdapters(host.shapeHost, tuple);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
|
||||
`entry:`,
|
||||
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
|
||||
` %tuple = call ptr @${host.callTarget(fnName)}(ptr %entries)`,
|
||||
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
registerReplay("allTupleAdapterFor", (host, args) =>
|
||||
allTupleAdapterFor(host, args[0] as string, args[1] as IrType),
|
||||
);
|
||||
|
||||
/** Promise.all's tuple form (promise.allTuple): the combinator over the
|
||||
* entries stores each fulfilled entry promise, and a settlement adapter on
|
||||
* the combinator calls the builder with them and fulfills the result with
|
||||
@@ -1081,26 +1173,7 @@ function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape");
|
||||
const entriesT = e.args[0]!.type;
|
||||
const entries = host.emitExpr(e.args[0]!);
|
||||
const tuple = e.type.inner;
|
||||
const key = `allTuple:${build.fnName}`;
|
||||
let adapter = host.resolveThunks.get(key);
|
||||
if (adapter === undefined) {
|
||||
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, adapter);
|
||||
const rc = vAdapters(host.shapeHost, tuple);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
|
||||
`entry:`,
|
||||
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
|
||||
` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`,
|
||||
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
const adapter = allTupleAdapterFor(host, build.fnName, e.type.inner);
|
||||
host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`);
|
||||
host.declare(`declare ptr @scr_promise_new()`);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
callbackType,
|
||||
finishRuntimeCall,
|
||||
ptr,
|
||||
caBytes,
|
||||
rawBytes,
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
@@ -204,7 +205,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
);
|
||||
return finishRuntimeCall(host, e, "scr_http2_connect", [
|
||||
...a.slice(0, 2),
|
||||
...rawBytes(host, args[2]!),
|
||||
...caBytes(host, args[2]!),
|
||||
...handler,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js
|
||||
import { f64Lit } from "./common.js";
|
||||
import { mockTimerSym } from "./lib-shared.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -498,6 +499,7 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
|
||||
if (sym) return sym;
|
||||
sym = `sc_alsbound_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "boundCallbackThunk", [cbT]);
|
||||
host.declare("declare ptr @scr_als_bound_target(ptr)");
|
||||
host.declare("declare ptr @scr_als_bound_enter(ptr)");
|
||||
host.declare("declare void @scr_als_bound_leave(ptr, ptr)");
|
||||
@@ -517,9 +519,14 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("boundCallbackThunk", (host, args) =>
|
||||
boundCallbackThunk(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
|
||||
export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "process.forkTarget") {
|
||||
|
||||
@@ -37,9 +37,11 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
|
||||
|
||||
export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
|
||||
"assert.throwsValidate": "scr_assert_throws_validate",
|
||||
"insp.jsonDyn": "scr_util_format_j",
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
"intl.segmenterRefusing": "scr_intl_segmenter_refusing",
|
||||
"weakMap.is": "scr_weak_map_is",
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
@@ -82,6 +84,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"util.isDeepStrictEqual": "scr_util_is_deep_strict_equal",
|
||||
"util.styleText": "scr_util_style_text",
|
||||
"util.inherits": "scr_util_inherits",
|
||||
"util.inheritsPrototype": "scr_util_inherits_prototype",
|
||||
"util.deprecate": "scr_util_deprecate",
|
||||
"util.callbackify": "scr_util_callbackify",
|
||||
"util.promisifyDyn": "scr_util_promisify",
|
||||
@@ -254,6 +257,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"global.native": "scr_global_native",
|
||||
"process.builtinModule": "scr_process_builtin_module",
|
||||
"process.builtinUnsupported": "scr_process_builtin_unsupported",
|
||||
"module.namespaceValue": "scr_module_namespace_value",
|
||||
"fs.callbackValue": "scr_fs_callback_value",
|
||||
"fs.callbackCall": "scr_fs_callback_call",
|
||||
"process.hrtimeValue": "scr_process_hrtime_value",
|
||||
@@ -334,6 +338,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getTime": "scr_date_get_time",
|
||||
"date.valueOf": "scr_date_get_time",
|
||||
"date.fromTime": "scr_date_get_time",
|
||||
"date.setFields": "scr_date_set_fields",
|
||||
"date.timeClip": "scr_date_new_ms",
|
||||
"date.parseGetTime": "scr_date_parse",
|
||||
"date.utc": "scr_date_utc",
|
||||
"date.newLocal": "scr_date_new_local",
|
||||
@@ -399,6 +405,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.eqBool": "scr_assert_eq_bool",
|
||||
"assert.eqSym": "scr_assert_eq_sym",
|
||||
"assert.eqDyn": "scr_assert_eq_dyn",
|
||||
"assert.messageValue": "scr_assert_message_value",
|
||||
"assert.methodValue": "scr_assert_method_value",
|
||||
"assert.messageClear": "scr_assert_message_clear",
|
||||
"assert.looseResult": "scr_assert_loose_result",
|
||||
"assert.deepResult": "scr_assert_deep_result",
|
||||
"assert.looseDeepResult": "scr_assert_loose_deep_result",
|
||||
@@ -523,6 +532,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fs.openSync": "scr_fs_open",
|
||||
"fs.openNumericSync": "scr_fs_open_numeric",
|
||||
"fs.readSync": "scr_fs_read_sync",
|
||||
"fs.readSyncDyn": "scr_fs_read_sync_dyn",
|
||||
"fs.writeSync": "scr_fs_write_sync",
|
||||
"fs.writeStrSync": "scr_fs_write_str_sync",
|
||||
"fs.closeSync": "scr_fs_close",
|
||||
@@ -795,6 +805,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"procStream.write": "scr_proc_stream_write",
|
||||
"stream.stdioWritable": "scr_stream_stdio_writable",
|
||||
"stream.unboxDyn": "scr_stream_unbox_dyn",
|
||||
"stream.dynIterableSource": "scr_stream_dyn_iterable_source",
|
||||
"stream.handleStream": "scr_stream_handle",
|
||||
"stream.pipeDyn": "scr_stream_pipe_dyn",
|
||||
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
|
||||
@@ -987,6 +998,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.keySetComputed": "scr_dyn_key_set_computed",
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
"dyn.keyDeleteComputed": "scr_dyn_key_delete_computed",
|
||||
"dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property",
|
||||
"dyn.hasKeyComputed": "scr_dyn_has_key_computed",
|
||||
"dyn.hasOwnComputed": "scr_dyn_has_own_computed",
|
||||
"dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed",
|
||||
@@ -1052,6 +1064,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.nativeDateHandleIs": "scr_dyn_native_date_is",
|
||||
"dyn.globalConstructor": "scr_dyn_global_constructor",
|
||||
"dyn.builtinFunction": "scr_dyn_builtin_function",
|
||||
"dyn.namespaceNew": "scr_dyn_namespace_new",
|
||||
"dyn.namespaceDefine": "scr_dyn_namespace_define",
|
||||
"date.nativeNew": "scr_dyn_native_date_new",
|
||||
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
|
||||
"date.nativeNewValue": "scr_dyn_native_date_new_value",
|
||||
@@ -1091,6 +1105,28 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
|
||||
"crypto.createSign": "scr_crypto_create_sign",
|
||||
"crypto.createVerify": "scr_crypto_create_verify",
|
||||
"crypto.x509Raw": "scr_crypto_x509_raw",
|
||||
"crypto.x509RawDer": "scr_crypto_x509_raw_der",
|
||||
"crypto.x509RawStr": "scr_crypto_x509_raw_str",
|
||||
"crypto.x509Subject": "scr_crypto_x509_subject",
|
||||
"crypto.x509Issuer": "scr_crypto_x509_issuer",
|
||||
"crypto.x509SubjectAltName": "scr_crypto_x509_subject_alt_name",
|
||||
"crypto.x509InfoAccess": "scr_crypto_x509_info_access",
|
||||
"crypto.x509SerialNumber": "scr_crypto_x509_serial_number",
|
||||
"crypto.x509Fingerprint256": "scr_crypto_x509_fingerprint256",
|
||||
"crypto.x509Fingerprint512": "scr_crypto_x509_fingerprint512",
|
||||
"crypto.x509Ca": "scr_crypto_x509_ca",
|
||||
"crypto.x509SignatureAlgorithm": "scr_crypto_x509_signature_algorithm",
|
||||
"crypto.x509SignatureAlgorithmOid": "scr_crypto_x509_signature_algorithm_oid",
|
||||
"crypto.x509CheckHost": "scr_crypto_x509_check_host",
|
||||
"crypto.x509CheckEmail": "scr_crypto_x509_check_email",
|
||||
"crypto.x509CheckIP": "scr_crypto_x509_check_ip",
|
||||
"crypto.x509CheckIssued": "scr_crypto_x509_check_issued",
|
||||
"crypto.x509LegacyObject": "scr_crypto_x509_legacy_object",
|
||||
"crypto.x509Pem": "scr_crypto_x509_pem",
|
||||
"crypto.x509PublicKey": "scr_crypto_x509_public_key",
|
||||
"crypto.x509Verify": "scr_crypto_x509_verify",
|
||||
"crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key",
|
||||
"fs.writeFileModeSync": "scr_fs_write_file_mode",
|
||||
"fs.chownSync": "scr_fs_chown",
|
||||
"fs.lchownSync": "scr_fs_lchown",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
finishRuntimeCall,
|
||||
nullableReference,
|
||||
ptr,
|
||||
caBytes,
|
||||
rawBytes,
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
@@ -110,6 +111,31 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_cipher", a);
|
||||
case "tls.sockAlpnProtocol":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_alpn_protocol", a);
|
||||
case "tls.serverAddContext":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_add_context", [...a, ptr()]);
|
||||
case "tls.serverAddContextCtx":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_add_context", [a[0]!, a[1]!, ptr(), a[2]!]);
|
||||
case "tls.serverSetSecureContext":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_set_secure_context", a);
|
||||
case "tls.serverOnClientError": {
|
||||
// (err, tlsSocket) listeners by arity: the adapter retains what the
|
||||
// listener takes.
|
||||
const cb = args[1]!;
|
||||
const thunk = `scr_net_client_error_thunk${callbackType(cb).params.length}`;
|
||||
return finishRuntimeCall(host, e, "scr_net_server_on_client_error", [
|
||||
a[0]!,
|
||||
...callbackAdapter(host, cb, thunk, ["ptr", "ptr", "ptr"]),
|
||||
a[2]!,
|
||||
]);
|
||||
}
|
||||
case "tls.sockGetPeerCertificate":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_peer_certificate", a);
|
||||
case "tls.sockGetCertificate":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_certificate", a);
|
||||
case "tls.sockPeerCertDer":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_peer_cert_der", a);
|
||||
case "tls.sockOwnCertDer":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_own_cert_der", a);
|
||||
case "tls.sockAuthError": {
|
||||
const result = callRuntime(host, "scr_tls_sock_auth_error", "ptr", a);
|
||||
return nullableReference(host, e.type, result, "string", "nullT");
|
||||
@@ -159,7 +185,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
);
|
||||
return finishRuntimeCall(host, e, "scr_https_request_agent", [
|
||||
...a.slice(0, 8),
|
||||
...rawBytes(host, args[8]!),
|
||||
...caBytes(host, args[8]!),
|
||||
a[9]!,
|
||||
...handler,
|
||||
]);
|
||||
@@ -179,7 +205,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
["ptr", "ptr"],
|
||||
);
|
||||
// Evaluate arguments once before selecting which transport consumes the callback.
|
||||
const pem = rawBytes(host, args[9]!);
|
||||
const pem = caBytes(host, args[9]!);
|
||||
const B = host.B,
|
||||
tls = B.newLabel("request.tls"),
|
||||
plain = B.newLabel("request.plain"),
|
||||
|
||||
@@ -113,7 +113,8 @@ test("conditional and reassigned locals own payload snapshots without heap wrapp
|
||||
expect(facts(mod).get("item")?.ownerType).toEqual(STRING);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("call ptr @sc_map_entry_f64("); // inline number-key read
|
||||
// The inline number-key read, on the integer view of the literal key.
|
||||
expect(ir).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 \d+, double /);
|
||||
expect(ir).toContain("@scr_str_release");
|
||||
expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/);
|
||||
expect(ir).toContain("store i64 0");
|
||||
|
||||
@@ -118,7 +118,7 @@ test("string keys and reference values are retained inline, boolean slots compar
|
||||
});
|
||||
|
||||
/** work(map, k): map.has(k) on a number-keyed map. */
|
||||
function lookupFixture(): IrModule {
|
||||
function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
const params = [
|
||||
{ localId: "map", name: "map", type: map },
|
||||
@@ -128,7 +128,7 @@ function lookupFixture(): IrModule {
|
||||
kind: "mapIntrinsic",
|
||||
method: "has",
|
||||
receiver: ref("map", map),
|
||||
args: [ref("k", F64)],
|
||||
args: [key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
@@ -182,6 +182,79 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
}
|
||||
});
|
||||
|
||||
test("a key with an integer view tests the direct index on the integer", () => {
|
||||
for (const bits of [32, 64] as const) {
|
||||
const sz = bits === 64 ? "i64" : "i32";
|
||||
const integral = emitted(lookupFixture({ kind: "numLit", value: 12, type: F64, loc }), bits);
|
||||
expect(integral.work).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 12, double /);
|
||||
const helper =
|
||||
/^define internal ptr @sc_map_entry_f64_int\(ptr %m, i64 %ki, double %k\)[^]*?^}/m.exec(
|
||||
integral.module,
|
||||
)![0];
|
||||
// An unsigned compare also rejects negative keys; the hashed probe takes
|
||||
// the double.
|
||||
expect(helper).toContain("icmp ult i64 %ki, %nd");
|
||||
expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`);
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)");
|
||||
expect(helper).not.toContain("fptosi");
|
||||
// A fractional key and a key without a proof keep the double lookup.
|
||||
for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) {
|
||||
const { work } = emitted(lookupFixture(key), bits);
|
||||
expect(work).toContain("call ptr @sc_map_entry_f64(");
|
||||
expect(work).not.toContain("@sc_map_entry_f64_int(");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("a key view converted in a branch is not reused by a later lookup of the same constant", () => {
|
||||
// work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40).
|
||||
const wide = 2 ** 40;
|
||||
const mod = lookupFixture();
|
||||
const work = mod.functions[1]!;
|
||||
const map = work.params[0]!.type;
|
||||
const has = (key: IrExpr): IrExpr => ({
|
||||
kind: "mapIntrinsic",
|
||||
method: "has",
|
||||
receiver: ref("map", map),
|
||||
args: [key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const literal: IrExpr = { kind: "numLit", value: wide, type: F64, loc };
|
||||
const taken = { localId: "taken", name: "taken", type: BOOL };
|
||||
work.params.push(taken);
|
||||
work.locals.push({ id: "taken", name: "taken", type: BOOL, mutable: true });
|
||||
work.body = [
|
||||
{
|
||||
kind: "if",
|
||||
cond: ref("taken", BOOL),
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: has({
|
||||
kind: "assignExpr",
|
||||
localId: "k",
|
||||
value: literal,
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: has(literal), loc },
|
||||
];
|
||||
for (const bits of [32, 64] as const) {
|
||||
const { work: text } = emitted(mod, bits);
|
||||
// The lookup after the branch takes the literal, not a conversion made
|
||||
// inside the branch.
|
||||
expect(text).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 1099511627776, double /);
|
||||
expect(text).not.toMatch(/@sc_map_entry_f64_int\(ptr %\S+, i64 %/);
|
||||
}
|
||||
});
|
||||
|
||||
/** work(map): map.size, and keep(map): map returned as an owned value. */
|
||||
function sizeFixture(): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
|
||||
@@ -28,16 +28,44 @@
|
||||
* guard excludes. for-of starts at scr_map_iter_start, past a deleted
|
||||
* prefix.
|
||||
*/
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { mapKeyAccess } from "./shapes.js";
|
||||
|
||||
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
|
||||
/** Record the integer view of a number Map/Set key operand for the entry
|
||||
* lookups of that key, when the view needs no instruction of its own: the
|
||||
* value's integer or uint32 view, defined with the value, or an integral
|
||||
* literal. A view exactInteger converts at this lookup is not recorded: the
|
||||
* key cache spans the function, and a later lookup of the same operand (a
|
||||
* constant, say) can be in a block the conversion does not dominate. */
|
||||
export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: IrExpr): void {
|
||||
if (key.type.kind !== "f64" || host.numberMapKeyIntegers.has(key.name)) return;
|
||||
if (!key.integer && key.uint32 === undefined && expr.kind !== "numLit") return;
|
||||
const integer = exactInteger(host, key, expr);
|
||||
if (!integer) return;
|
||||
const defined =
|
||||
integer.name === key.integer?.name ||
|
||||
integer.name === key.uint32 ||
|
||||
/^-?\d+$/.test(integer.name);
|
||||
if (defined) host.numberMapKeyIntegers.set(key.name, integer);
|
||||
}
|
||||
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation. */
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation.
|
||||
* A key with an integer view (noteNumberMapKey) tests the direct index on
|
||||
* that integer instead of converting the double back. */
|
||||
export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(NUMBER_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
const integer = host.numberMapKeyIntegers.get(key);
|
||||
if (integer) {
|
||||
const wide = widenInteger(host, integer);
|
||||
host.B.line(
|
||||
`${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
);
|
||||
return entry;
|
||||
}
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`);
|
||||
return entry;
|
||||
}
|
||||
@@ -122,14 +150,16 @@ export function emitStringMapGetScalar(
|
||||
}
|
||||
|
||||
/** Map.get of a reference value: a +1 value, or null when absent — the
|
||||
* contract of scr_map_get_f64_ref. */
|
||||
* contract of scr_map_get_f64_ref. `borrow`: the entry's own reference,
|
||||
* without a retain, for a reader that finishes before the map can change. */
|
||||
export function emitNumberMapGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
value: IrType,
|
||||
borrow = false,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value);
|
||||
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value, false, borrow);
|
||||
}
|
||||
|
||||
/** Map.get of a reference value under a string key, with scr_map_get_str_ref's
|
||||
@@ -141,16 +171,25 @@ export function emitStringMapGetRef(
|
||||
key: string,
|
||||
value: IrType,
|
||||
compact = false,
|
||||
borrow = false,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact);
|
||||
return emitMapEntryGetRef(
|
||||
host,
|
||||
emitStringMapEntry(host, map, key, compact),
|
||||
value,
|
||||
compact,
|
||||
borrow,
|
||||
);
|
||||
}
|
||||
|
||||
/** The +1 value of a found entry, or null for a missing (null) entry. */
|
||||
/** The +1 value of a found entry (its own reference when `borrow`), or
|
||||
* null for a missing (null) entry. */
|
||||
function emitMapEntryGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
entry: string,
|
||||
value: IrType,
|
||||
compact = false,
|
||||
borrow = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const found = B.tmp();
|
||||
@@ -160,10 +199,8 @@ function emitMapEntryGetRef(
|
||||
join = B.newLabel("mapn.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const retained = host.retainFrame(
|
||||
compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"),
|
||||
value,
|
||||
);
|
||||
const loaded = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr");
|
||||
const retained = borrow ? loaded : host.retainFrame(loaded, value);
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
|
||||
@@ -120,7 +120,12 @@ test("local map results keep scalar and owned reference payloads without heap wr
|
||||
expect(ir).toContain("call ptr @sc_map_entry_str(");
|
||||
expect(ir).not.toContain("@scr_map_get_");
|
||||
}
|
||||
if (value === STRING) expect(ir).toContain("@scr_str_release");
|
||||
// Nothing can change the map before the tag test, so a reference
|
||||
// result is the entry's own (findMapReadBorrows): no retain or release.
|
||||
if (value === STRING) {
|
||||
expect(ir).not.toContain("@scr_str_retain");
|
||||
expect(ir).not.toContain("@scr_str_release");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -132,7 +137,8 @@ test("nullable record results are the looked-up pointer itself on both ABIs", ()
|
||||
expect(ir).not.toContain("%ScrUnion");
|
||||
expect(ir).not.toContain("@scr_union_");
|
||||
expect(ir).toMatch(/icmp (?:eq|ne) ptr %\w+, null/);
|
||||
expect(ir).toContain("@sc_rrelease_");
|
||||
// The pointer is the entry's own reference until the tag test.
|
||||
expect(ir).not.toContain("@sc_rrelease_");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExpr, everyStmt, everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import {
|
||||
emitNumberMapGetRef,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapGetRef,
|
||||
compactMapValues,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
import { rememberMapEntry } from "./map-entry-fusion.js";
|
||||
@@ -68,6 +70,10 @@ export function borrowsMapMutationReceiver(expr: IrExpr): boolean {
|
||||
export interface LocalMapRead {
|
||||
/** The Map.get expression itself. */
|
||||
expr: IrExpr;
|
||||
/** A local's reference result may stay the entry's own reference: every
|
||||
* use of the local runs before anything that can change a reference
|
||||
* edge (findMapReadBorrows), so the entry still holds the value. */
|
||||
borrowed?: boolean;
|
||||
type: IrType;
|
||||
receiver: IrExpr;
|
||||
key: IrExpr;
|
||||
@@ -126,6 +132,7 @@ export function findMapReadLifetimes(
|
||||
fn: IrFunction,
|
||||
unions: ReadonlyMap<string, IrUnionDef>,
|
||||
lifetimes: CallLifetimes,
|
||||
effects?: ReferenceEffects,
|
||||
): MapReadLifetimes {
|
||||
const result: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -150,6 +157,125 @@ export function findMapReadLifetimes(
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (effects) {
|
||||
const borrowed = findMapReadBorrows(fn, effects, (decl) => {
|
||||
const read = result.locals.get(decl.localId);
|
||||
return read !== undefined && isRefCounted(read.value);
|
||||
});
|
||||
for (const id of borrowed) result.locals.get(id)!.borrowed = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether a map read's receiver keeps the map past its own statement: a
|
||||
* local or a field of one. Changing either is an edge change the borrow
|
||||
* proof sees. A temporary map (a call's result, `new Map()`) is released at
|
||||
* the end of the declaration, and its entries with it. */
|
||||
function heldReceiver(expr: IrExpr): boolean {
|
||||
if (expr.kind === "varRef") return true;
|
||||
if (expr.kind === "fieldGet" || expr.kind === "recordGet") return heldReceiver(expr.obj);
|
||||
if (expr.kind === "unionNarrow" || expr.kind === "downcast") return heldReceiver(expr.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The locals, declared by statements `candidate` accepts, that can hold a
|
||||
* map entry's own reference instead of a +1 copy. The statements after the
|
||||
* declaration run in order, and every one that names the local must
|
||||
* preserve reference edges (ReferenceEffects) and run before any statement
|
||||
* that may not, so no write, deletion or unknown call can release the value
|
||||
* while the local is read. An `if` passes its condition's state to both
|
||||
* branches and joins them; any other statement that names the local is
|
||||
* judged whole. A later pass of an enclosing loop declares the local again,
|
||||
* so only the rest of its own statement list matters. The map must be held
|
||||
* by a local or a field (heldReceiver), the read itself must preserve
|
||||
* reference edges (a key whose evaluation replaces the receiver's field
|
||||
* leaves the read on a map the statement releases), and locals declared
|
||||
* from the local count as its uses. An update such as
|
||||
* `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps
|
||||
* the borrow: the write runs only on the branch that does not read `c`. */
|
||||
export function findMapReadBorrows(
|
||||
fn: IrFunction,
|
||||
effects: ReferenceEffects,
|
||||
candidate: (decl: IrStmt & { kind: "varDecl" }) => boolean,
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
// `ids` holds the local and the locals declared from it: a stable alias
|
||||
// (`const d = c`) reads the same entry without a reference of its own.
|
||||
const names = (node: IrStmt | IrExpr, ids: ReadonlySet<string>, isStmt: boolean): boolean => {
|
||||
let found = false;
|
||||
const visitor = {
|
||||
stmt: () => !found,
|
||||
expr: (expr: IrExpr) => {
|
||||
if (expr.kind === "varRef" && ids.has(expr.localId)) found = true;
|
||||
return !found;
|
||||
},
|
||||
};
|
||||
if (isStmt) everyStmt(node as IrStmt, visitor);
|
||||
else everyExpr(node as IrExpr, visitor);
|
||||
return found;
|
||||
};
|
||||
// Adds the reference locals that `stmt` declares from one in `ids`, at any
|
||||
// depth: a `var` inside a loop or a try stays in scope after it.
|
||||
const alias = (stmt: IrStmt, ids: Set<string>): void => {
|
||||
everyStmt(stmt, {
|
||||
stmt: (node) => {
|
||||
if (
|
||||
node.kind === "varDecl" &&
|
||||
node.init !== null &&
|
||||
isRefCounted(node.init.type) &&
|
||||
names(node.init, ids, false)
|
||||
)
|
||||
ids.add(node.localId);
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
};
|
||||
// Whether an edge may have changed after `list`, or null when a statement
|
||||
// names the local after such a change (or changes edges itself).
|
||||
const walk = (list: readonly IrStmt[], ids: Set<string>, changed: boolean): boolean | null => {
|
||||
for (const stmt of list) {
|
||||
if (stmt.kind === "if") {
|
||||
const preserving = effects.preserves(stmt.cond);
|
||||
if (names(stmt.cond, ids, false) && (changed || !preserving)) return null;
|
||||
const condition = changed || !preserving;
|
||||
const then = walk(stmt.then, ids, condition);
|
||||
const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, ids, condition);
|
||||
if (then === null || otherwise === null) return null;
|
||||
changed = then || otherwise;
|
||||
continue;
|
||||
}
|
||||
if (stmt.kind === "block") {
|
||||
const inner = walk(stmt.body, ids, changed);
|
||||
if (inner === null) return null;
|
||||
changed = inner;
|
||||
continue;
|
||||
}
|
||||
const preserving = effects.preservesScope([stmt]);
|
||||
if (names(stmt, ids, true)) {
|
||||
if (changed || !preserving) return null;
|
||||
alias(stmt, ids);
|
||||
}
|
||||
changed ||= !preserving;
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
const visit = (list: readonly IrStmt[]): void => {
|
||||
list.forEach((stmt, index) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
stmt.init?.kind === "mapIntrinsic" &&
|
||||
heldReceiver(stmt.init.receiver) &&
|
||||
effects.preserves(stmt.init) &&
|
||||
candidate(stmt) &&
|
||||
walk(list.slice(index + 1), new Set([stmt.localId]), false) !== null
|
||||
)
|
||||
result.add(stmt.localId);
|
||||
for (const nested of nestedStmtLists(stmt)) visit(nested);
|
||||
});
|
||||
};
|
||||
visit(fn.body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -265,6 +391,7 @@ export function emitMapLookupKey(
|
||||
const value = borrow ? host.emitReadReceiver(key) : host.emitExpr(key);
|
||||
const access = mapKeyAccess(key.type);
|
||||
const type = mapKeyParamType(access);
|
||||
if (access === "f64") noteNumberMapKey(host, value, key);
|
||||
return { access, types: type, args: `${type} ${value.name}`, value: value.name };
|
||||
}
|
||||
|
||||
@@ -303,13 +430,14 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
let owner: StackMapRead["owner"] = null;
|
||||
const number = inlineNumberKey(key);
|
||||
const string = entryStringKey(key);
|
||||
const borrow = read.borrowed === true;
|
||||
if (number !== null) {
|
||||
if (isRefCounted(read.value)) {
|
||||
const raw = emitNumberMapGetRef(host, receiver.name, number, read.value);
|
||||
const raw = emitNumberMapGetRef(host, receiver.name, number, read.value, borrow);
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
owner = { slot: payload, type: read.value };
|
||||
if (!borrow) owner = { slot: payload, type: read.value };
|
||||
} else {
|
||||
// Booleans are stored as 0/1, already the widened union payload.
|
||||
const scalar = emitNumberMapGetScalar(host, receiver.name, number);
|
||||
@@ -327,8 +455,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
let raw: string;
|
||||
const mapType = read.receiver.type;
|
||||
const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value);
|
||||
// The runtime's other key kinds return +1, which the frame then owns.
|
||||
const borrowed = borrow && stringKey !== null;
|
||||
if (stringKey !== null)
|
||||
raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact);
|
||||
raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact, borrowed);
|
||||
else {
|
||||
raw = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
|
||||
@@ -337,7 +467,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
owner = { slot: payload, type: read.value };
|
||||
if (!borrowed) owner = { slot: payload, type: read.value };
|
||||
} else {
|
||||
const scalar = read.value.kind === "f64" ? "f64" : "bool";
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
|
||||
@@ -56,10 +56,11 @@ test("unions of two or more records become shape unions", () => {
|
||||
});
|
||||
|
||||
test("records of runtime-inspected unions keep every union over them boxed", () => {
|
||||
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
|
||||
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "abf" } };
|
||||
const unions = new NullableUnions(
|
||||
module(
|
||||
[
|
||||
{ id: "abf", arms: [a, b, F64] },
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abu", arms: [a, b, UNDEFINED_T] },
|
||||
{ id: "bc", arms: [b, c] },
|
||||
@@ -68,6 +69,7 @@ test("records of runtime-inspected unions keep every union over them boxed", ()
|
||||
{ globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
|
||||
),
|
||||
);
|
||||
expect(unions.get("abf")).toBeNull();
|
||||
expect(unions.get("ab")).toBeNull();
|
||||
expect(unions.get("abu")).toBeNull();
|
||||
expect(unions.get("bc")).toBeNull();
|
||||
@@ -75,6 +77,34 @@ test("records of runtime-inspected unions keep every union over them boxed", ()
|
||||
expect(unions.shapeNumber("c")).toBe(0);
|
||||
});
|
||||
|
||||
test("unions of records alone stay shape unions as Map keys and Set elements", () => {
|
||||
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
|
||||
const map: IrType = { kind: "map", key: { kind: "union", unionId: "abc" }, value: F64 };
|
||||
const unions = new NullableUnions(
|
||||
module(
|
||||
[
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abc", arms: [a, b, c] },
|
||||
{ id: "ba", arms: [b, a] },
|
||||
{ id: "abu", arms: [a, b, UNDEFINED_T] },
|
||||
],
|
||||
{
|
||||
globals: [
|
||||
{ id: "%g.s", name: "s", type: set, mutable: false },
|
||||
{ id: "%g.m", name: "m", type: map, mutable: false },
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(unions.shapes("ab")).not.toBeNull();
|
||||
expect(unions.shapes("abc")).not.toBeNull();
|
||||
expect(unions.shapes("abu")).not.toBeNull();
|
||||
// Unions over the same records share one release and trace helper.
|
||||
expect(unions.helperOwner("ba")).toBe("ab");
|
||||
expect(unions.helperOwner("abu")).toBe("ab");
|
||||
expect(unions.helperOwner("abc")).toBe("abc");
|
||||
});
|
||||
|
||||
test("library, lifetime-region and debug builds keep record unions boxed", () => {
|
||||
const unions = [{ id: "ab", arms: [a, b] }];
|
||||
expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
|
||||
|
||||
@@ -118,6 +118,9 @@ export class NullableUnions {
|
||||
readonly retainWrappers = new Map<string, string>();
|
||||
/** Some nullable union encodes `null` with NULLABLE_NULL. */
|
||||
readonly usesNullSentinel: boolean = false;
|
||||
/** The shape union whose release and trace helpers each shape union
|
||||
* uses: the first one over the same record arms. */
|
||||
private readonly helperOwners = new Map<string, string>();
|
||||
|
||||
/** `shapes`: allow shape unions (debug builds keep boxes, whose
|
||||
* debug-info layouts describe every arm). */
|
||||
@@ -151,6 +154,24 @@ export class NullableUnions {
|
||||
if (!this.shapeNumbers.has(arm.shapeId))
|
||||
this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
|
||||
}
|
||||
// Release and trace dispatch on the shape word, which names a record
|
||||
// shape module-wide, so unions over the same records share them.
|
||||
const owners = new Map<string, string>();
|
||||
for (const nullable of this.byId.values()) {
|
||||
if (nullable.repr !== "shapes") continue;
|
||||
const key = nullable.arms
|
||||
.map((arm) => arm.shapeId)
|
||||
.sort()
|
||||
.join(" ");
|
||||
const owner = owners.get(key) ?? nullable.unionId;
|
||||
owners.set(key, owner);
|
||||
this.helperOwners.set(nullable.unionId, owner);
|
||||
}
|
||||
}
|
||||
|
||||
/** The shape union whose release and trace helpers this one uses. */
|
||||
helperOwner(unionId: string): string {
|
||||
return this.helperOwners.get(unionId) ?? unionId;
|
||||
}
|
||||
|
||||
get(unionId: string): PointerUnion | null {
|
||||
@@ -277,13 +298,19 @@ function shapeUnion(
|
||||
|
||||
/** Unions the runtime itself inspects as tagged boxes keep that
|
||||
* representation: Map keys and Set elements hash and compare union keys by
|
||||
* tag and payload inside the C map. */
|
||||
* tag and payload inside the C map. A union of records alone is the
|
||||
* exception: its keys compare by record identity, which a shape union's
|
||||
* record pointer carries as well (the map keys it by reference). */
|
||||
function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
|
||||
const boxed = new Set<string>();
|
||||
// Only the module's union definitions consult the result.
|
||||
if (!mod.unions?.length) return boxed;
|
||||
const recordsOnly = new Set<string>();
|
||||
for (const def of mod.unions)
|
||||
if (def.arms.length >= 2 && def.arms.every((arm) => arm.kind === "record"))
|
||||
recordsOnly.add(def.id);
|
||||
const mark = (t: IrType): void => {
|
||||
if (t.kind === "union") boxed.add(t.unionId);
|
||||
if (t.kind === "union" && !recordsOnly.has(t.unionId)) boxed.add(t.unionId);
|
||||
};
|
||||
if (nodes !== undefined) {
|
||||
for (const t of nodes.types) {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { BOOL, F64, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
const loc = { file: "record-union-keys.ts", start: 0, end: 0 };
|
||||
|
||||
async function llvmFor(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-record-union-keys-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, {
|
||||
outDir: dir,
|
||||
outPath: output,
|
||||
outputKind: "ir",
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return emitLlvmModule(module);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function body(ir: string, name: string): string {
|
||||
const found = new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir);
|
||||
expect(found, name).not.toBeNull();
|
||||
return found![0];
|
||||
}
|
||||
|
||||
test("Sets and Maps of record unions key the record pointer", async () => {
|
||||
const ir = await llvmFor(`
|
||||
type Num = { kind: "num"; value: number };
|
||||
type Neg = { kind: "neg"; operand: Node };
|
||||
type Add = { kind: "add"; left: Node; right: Node };
|
||||
type Node = Num | Neg | Add;
|
||||
const one: Node = { kind: "num", value: 1 };
|
||||
const nodes: Node[] = [one, { kind: "neg", operand: one }, { kind: "add", left: one, right: one }];
|
||||
const seen = new Set<Node>();
|
||||
const depth = new Map<Node, number>();
|
||||
for (const node of nodes) {
|
||||
seen.add(node);
|
||||
depth.set(node, node.kind === "num" ? 0 : 1);
|
||||
}
|
||||
console.log(seen.size, depth.get(nodes[1]!), seen.has({ kind: "num", value: 1 }));
|
||||
`);
|
||||
// SCR_MAP_KEY_REF (2), never SCR_MAP_KEY_UNION_REF (3), and no union box.
|
||||
expect(ir).toMatch(/call ptr @scr_map_new\(i32 2,/);
|
||||
expect(ir).not.toMatch(/call ptr @scr_map_new\(i32 3,/);
|
||||
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
|
||||
});
|
||||
|
||||
const recordType = (shapeId: string): IrType => ({ kind: "record", shapeId });
|
||||
const wide: IrType = { kind: "union", unionId: "wide" };
|
||||
|
||||
/** `pick(u)` answers whether `u` holds arm 0, 2 or 3 of a five-record union
|
||||
* through an `||` chain of tag tests; `same` is a second union over the same
|
||||
* records in another order. */
|
||||
function tagChainModule(): IrModule {
|
||||
const u: IrExpr = { kind: "varRef", localId: "u", type: wide, loc };
|
||||
const isTag = (tag: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId: "wide",
|
||||
tag,
|
||||
negated: false,
|
||||
value: u,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const or = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const shapes = ["a", "b", "c", "d", "e"];
|
||||
return {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: shapes.map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
|
||||
unions: [
|
||||
{ id: "wide", arms: shapes.map(recordType) },
|
||||
{ id: "same", arms: [...shapes].reverse().map(recordType) },
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
name: "main",
|
||||
params: [],
|
||||
returnType: VOID,
|
||||
locals: [],
|
||||
body: [],
|
||||
loc,
|
||||
},
|
||||
{
|
||||
name: "pick",
|
||||
params: [{ localId: "u", name: "u", type: wide }],
|
||||
returnType: BOOL,
|
||||
locals: [{ id: "u", name: "u", type: wide, mutable: false }],
|
||||
body: [{ kind: "return", value: or(or(isTag(0), isTag(2)), isTag(3)), loc }],
|
||||
loc,
|
||||
},
|
||||
{
|
||||
name: "keep",
|
||||
params: [{ localId: "s", name: "s", type: { kind: "union", unionId: "same" } }],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
{
|
||||
id: "s",
|
||||
name: "s",
|
||||
type: { kind: "union", unionId: "same" },
|
||||
mutable: false,
|
||||
},
|
||||
],
|
||||
body: [],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("an || chain of tag tests is one switch on the shape word", () => {
|
||||
const mod = tagChainModule();
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const ir = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const pick = body(ir, "sc_bf_pick");
|
||||
const switches = pick.match(/switch i32 [^\n]*/g) ?? [];
|
||||
expect(switches).toHaveLength(1);
|
||||
expect(switches[0]!.match(/label %tags\.y/g)).toHaveLength(3);
|
||||
expect(pick).not.toMatch(/@sc_stag\./);
|
||||
});
|
||||
|
||||
test("unions over the same records share one release", () => {
|
||||
const ir = emitLlvmModule(tagChainModule(), { pointerBits: 64 });
|
||||
expect(ir.match(/^define internal void @sc_srelease\.[^\n]*noinline/gm)).toHaveLength(1);
|
||||
expect(ir).toMatch(/^define internal void @sc_srelease\.wide\(/m);
|
||||
expect(ir).not.toMatch(/^define internal void @sc_srelease\.same\(/m);
|
||||
});
|
||||
|
||||
test("a union with a freezable arm compares its count without the frozen bit", async () => {
|
||||
const ir = await llvmFor(`
|
||||
type Name = { kind: "name"; text: string };
|
||||
type Count = { kind: "count"; value: number };
|
||||
type Key = Name | Count;
|
||||
function name(text: string): Key {
|
||||
const built: Name = { kind: "name", text };
|
||||
return Object.freeze(built);
|
||||
}
|
||||
const seen = new Set<Key>();
|
||||
seen.add(name("a"));
|
||||
seen.add({ kind: "count", value: 1 });
|
||||
console.log(seen.size);
|
||||
`);
|
||||
const release = /^define internal void @sc_srelease\.[^\n]*\n[^]*?^}/m.exec(ir);
|
||||
expect(release).not.toBeNull();
|
||||
// SCR_RC_FROZEN is bit 62: a frozen record's last reference has count 1.
|
||||
expect(release![0]).toContain("and i64 %rc, -4611686018427387905");
|
||||
expect(release![0]).toContain("icmp ult i64 %rcm2, -4611686018427387907");
|
||||
});
|
||||
@@ -41,6 +41,8 @@ test("sort-heavy programs emit equal LLVM from memory and from serialized IR", a
|
||||
pointerBits: 64,
|
||||
wasi: false,
|
||||
runtimeAbiMarker: false,
|
||||
// compile() emits Linux textual LLVM with the unwinding ABI.
|
||||
unwindAbi: process.platform === "linux",
|
||||
objectAudit: sanitize,
|
||||
...inlineRcOption(options.optimization, sanitize),
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ export { computeTraced } from "../cycle-analysis.js";
|
||||
* `_v`-shaped. */
|
||||
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable);
|
||||
if (nullable) {
|
||||
const arm = vAdapters(host, nullable.arm);
|
||||
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
|
||||
@@ -290,7 +290,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
|
||||
export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
if (t.kind === "jsval") return jsvalRcSym(host, "retain");
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).retain;
|
||||
if (nullable) return nullableRetainSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
|
||||
@@ -305,10 +305,10 @@ export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
/** A shape union's RC entry points: every record retain is the same
|
||||
* NULL- and immortal-skipping increment (the null sentinel is immortal),
|
||||
* so the first arm's serves; release dispatches on the arm. */
|
||||
function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
|
||||
function shapeUnionRc(host: ShapeHost, union: ShapeUnion): { retain: string; release: string } {
|
||||
return {
|
||||
retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
|
||||
release: shapeReleaseFn(union.unionId),
|
||||
release: shapeReleaseFn(host.nullableUnions.helperOwner(union.unionId)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,38 +352,81 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
// release: unit encodings are NULL or immortal; each arm releases
|
||||
// through its own (NULL- and immortal-tolerant) helper.
|
||||
out.push(
|
||||
`define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
// Release and trace: one definition for the unions over the same
|
||||
// record arms (helperOwner). Unit encodings are NULL or the immortal
|
||||
// null sentinel; the shape word picks the arm.
|
||||
if (host.nullableUnions.helperOwner(union.unionId) !== union.unionId) continue;
|
||||
const units = [
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %${host.nullableUnions.usesNullSentinel ? "notnull" : "rec"}`,
|
||||
...(host.nullableUnions.usesNullSentinel
|
||||
? [
|
||||
`notnull:`,
|
||||
` %isnullarm = icmp eq ptr %o, ${NULLABLE_NULL}`,
|
||||
` br i1 %isnullarm, label %done, label %rec`,
|
||||
]
|
||||
: []),
|
||||
`rec:`,
|
||||
];
|
||||
const armSwitch = (call: (shapeId: string) => string): string[] => [
|
||||
` %wp = getelementptr inbounds ${header}, ptr %o, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` switch i32 %w, label %done [ ${union.arms.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [`a${arm.tag}:`, call(arm.shapeId), ` br label %done`]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
];
|
||||
// Every record arm shares the release fast path (the count at the same
|
||||
// offset, one collector header choice for the whole union), so only a
|
||||
// release that may free dispatches on the arm. The release stays a call:
|
||||
// inlined at every release site, it grows code more than it saves.
|
||||
const traced = union.arms.map((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
|
||||
// A freezable arm's count is the word without its frozen bit, as in
|
||||
// releaseFastPath: a frozen record's last reference takes the slow path.
|
||||
const frozen = union.arms.some((arm) => host.recordsById.get(arm.shapeId)?.freezable === true);
|
||||
const notFrozen = ~recordFrozenBit(S);
|
||||
const release = shapeReleaseFn(union.unionId);
|
||||
if (traced.every((t) => t === traced[0]))
|
||||
out.push(
|
||||
`define internal void ${release}(ptr %o) noinline ${FN_ATTRS} { ; shape union release`,
|
||||
`entry:`,
|
||||
...units,
|
||||
` %rc = load ${S}, ptr %o`,
|
||||
...(frozen
|
||||
? [
|
||||
` %refs = and ${S} %rc, ${sizeLiteral(S, notFrozen)}`,
|
||||
` %rcm2 = sub ${S} %refs, 2`,
|
||||
` %shared = icmp ult ${S} %rcm2, ${sizeLiteral(S, notFrozen - 2n)}`,
|
||||
]
|
||||
: [` %rcm2 = sub ${S} %rc, 2`, ` %shared = icmp ult ${S} %rcm2, -3`]),
|
||||
` br i1 %shared, label %dec, label %slow`,
|
||||
`dec:`,
|
||||
` %n = sub ${S} %rc, 1`,
|
||||
` store ${S} %n, ptr %o`,
|
||||
...(traced[0] ? cycleRootLines(host, "done") : [` br label %done`]),
|
||||
`slow:`,
|
||||
` %imm = icmp eq ${S} %rc, -1`,
|
||||
` br i1 %imm, label %done, label %free`,
|
||||
`free:`,
|
||||
...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}_slow(ptr %o)`),
|
||||
);
|
||||
else
|
||||
out.push(
|
||||
`define internal void ${release}(ptr %o) ${FN_ATTRS} { ; shape union release`,
|
||||
`entry:`,
|
||||
...units,
|
||||
...armSwitch((shapeId) => ` call void @${mangleRecordRelease(shapeId)}(ptr %o)`),
|
||||
);
|
||||
if (shapeUnionTraced(host, union))
|
||||
out.push(
|
||||
`define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
|
||||
`define internal void ${shapeTraceFn(union.unionId)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
...units,
|
||||
...armSwitch(
|
||||
(shapeId) => ` call void @${mangleRecordTrace(shapeId)}(ptr %o, ptr %visit, ptr %ctx)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
@@ -435,7 +478,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
|
||||
export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
if (t.kind === "jsval") return jsvalRcSym(host, "release");
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).release;
|
||||
if (nullable) return releaseSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
|
||||
@@ -985,7 +1028,9 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
|
||||
case "union": {
|
||||
const nullable = host.nullableUnions.get(t.unionId);
|
||||
if (nullable?.repr === "shapes")
|
||||
return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null;
|
||||
return shapeUnionTraced(host, nullable)
|
||||
? shapeTraceFn(host.nullableUnions.helperOwner(nullable.unionId))
|
||||
: null;
|
||||
if (nullable) return traceAdapter(host, nullable.arm);
|
||||
if (!host.tracedUnions.has(t.unionId)) return null;
|
||||
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
inlinableNumberFunction,
|
||||
isSpeculableValue,
|
||||
planProvenIntegerLoop,
|
||||
planSpeculativeLoop,
|
||||
rewriteSpeculable,
|
||||
speculativeReads,
|
||||
} from "./speculative-integers.js";
|
||||
|
||||
@@ -266,3 +268,136 @@ test("bounded cursors with proven writes are shadowed in a proven integer loop",
|
||||
planProvenIntegerLoop(forLoop([...body, jump]) as never, cursors, all, () => false, all),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
const libCall = (fn: string, args: IrExpr[]): IrExpr =>
|
||||
({ kind: "libCall", fn, args, type: F64, loc }) as never;
|
||||
const call = (callee: string, args: IrExpr[]): IrExpr => ({
|
||||
kind: "call",
|
||||
callee,
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const pow2 = (n: IrExpr): IrExpr => libCall("math.pow", [num(2), n]);
|
||||
/** `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }` */
|
||||
const trim = {
|
||||
name: "trim",
|
||||
params: [
|
||||
{ localId: "v", name: "v", type: F64 },
|
||||
{ localId: "n", name: "n", type: F64 },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: bin(
|
||||
"-",
|
||||
ref("v"),
|
||||
bin("*", libCall("math.floor", [bin("/", ref("v"), pow2(ref("n")))]), pow2(ref("n"))),
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
} as never;
|
||||
|
||||
test("calls of small pure number functions expand into a speculated write", () => {
|
||||
const inlinable = inlinableNumberFunction(trim)!;
|
||||
expect(inlinable.params).toEqual(["v", "n"]);
|
||||
const context = { inlinable: (callee: string) => (callee === "trim" ? inlinable : undefined) };
|
||||
// key = trim(key, m) * 4 + c
|
||||
const value = bin("+", bin("*", call("trim", [ref("key"), ref("m")]), num(4)), ref("c"));
|
||||
expect(isSpeculableValue(value)).toBe(false);
|
||||
const rewritten = rewriteSpeculable(value, context)!;
|
||||
expect(rewritten.leaves).toEqual([]);
|
||||
expect(JSON.stringify(rewritten.value)).not.toContain('"call"');
|
||||
const write = assign("key", value);
|
||||
const plan = planSpeculativeLoop(
|
||||
loop([write, increment("i")]) as never,
|
||||
all,
|
||||
() => false,
|
||||
() => false,
|
||||
all,
|
||||
context,
|
||||
)!;
|
||||
expect(plan.writes.has(write)).toBe(true);
|
||||
expect(plan.values.get(write)).toEqual(rewritten);
|
||||
// `m`, an input of the expanded body, is checked on entry with the others.
|
||||
expect([...plan.locals.keys()].sort()).toEqual(["c", "i", "key", "m", "n"]);
|
||||
});
|
||||
|
||||
test("Math.floor needs a power-of-two divisor, and expansion pure arguments", () => {
|
||||
const context = { inlinable: () => inlinableNumberFunction(trim) };
|
||||
expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(8))]), {})).not.toBeNull();
|
||||
expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), num(6))]), {})).toBeNull();
|
||||
expect(rewriteSpeculable(libCall("math.floor", [bin("/", ref("a"), ref("b"))]), {})).toBeNull();
|
||||
// An argument that is itself a call is evaluated once: no expansion.
|
||||
expect(rewriteSpeculable(call("trim", [call("other", []), ref("m")]), context)).toBeNull();
|
||||
const effectful = {
|
||||
...(trim as object),
|
||||
body: [{ kind: "exprStmt", expr: call("log", []), loc }],
|
||||
};
|
||||
expect(inlinableNumberFunction(effectful as never)).toBeUndefined();
|
||||
// A divisor next to a power of two is not one, though Math.log2 rounds it to 52.
|
||||
const near = (d: number): IrExpr => libCall("math.floor", [bin("/", ref("a"), num(d))]);
|
||||
expect(rewriteSpeculable(near(2 ** 52), {})).not.toBeNull();
|
||||
expect(rewriteSpeculable(near(2 ** 52 + 1), {})).toBeNull();
|
||||
expect(rewriteSpeculable(near(2 ** 53), {})).toBeNull();
|
||||
expect(rewriteSpeculable(near(0.5), {})).toBeNull();
|
||||
});
|
||||
|
||||
test("a helper's own calls must be of helpers that expand too", () => {
|
||||
// `function outer(v, n) { return trim(v, n) + other(v); }`
|
||||
const outer = {
|
||||
...(trim as object),
|
||||
name: "outer",
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: bin("+", call("trim", [ref("v"), ref("n")]), call("other", [ref("v")])),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
const helpers = (callee: string) =>
|
||||
callee === "trim" ? inlinableNumberFunction(trim) : undefined;
|
||||
expect(inlinableNumberFunction(outer as never)).toBeUndefined();
|
||||
expect(inlinableNumberFunction(outer as never, helpers)).toBeUndefined();
|
||||
const pure = {
|
||||
...(outer as object),
|
||||
body: [{ kind: "return", value: bin("*", call("trim", [ref("v"), ref("n")]), num(2)), loc }],
|
||||
};
|
||||
expect(inlinableNumberFunction(pure as never, helpers)).toBeDefined();
|
||||
});
|
||||
|
||||
test("other leaves are hoisted only when no call can change the write's locals", () => {
|
||||
let next = 0;
|
||||
const leafId = (): string => `%leaf.${next++}`;
|
||||
const value = bin(
|
||||
"%",
|
||||
bin("+", bin("*", ref("acc"), num(7)), call("noisy", [ref("i")])),
|
||||
num(1000003),
|
||||
);
|
||||
const hoisted = rewriteSpeculable(value, { unaliased: () => true, leafId })!;
|
||||
expect(hoisted.leaves.map((leaf) => leaf.expr)).toEqual([call("noisy", [ref("i")])]);
|
||||
expect(rewriteSpeculable(value, { unaliased: (id) => id !== "acc", leafId })).toBeNull();
|
||||
// A call whose argument writes a local stays where it is.
|
||||
const bump: IrExpr = { kind: "incDec", localId: "i", op: "+", prefix: false, type: F64, loc };
|
||||
expect(
|
||||
rewriteSpeculable(bin("+", ref("i"), call("noisy", [bump])), { unaliased: () => true, leafId }),
|
||||
).toBeNull();
|
||||
// Without leaf ids nothing is hoisted.
|
||||
expect(rewriteSpeculable(value, { unaliased: () => true })).toBeNull();
|
||||
const write = assign("acc", value);
|
||||
const plan = planSpeculativeLoop(
|
||||
loop([write, increment("i")]) as never,
|
||||
(id) => !id.startsWith("%leaf"),
|
||||
() => false,
|
||||
() => false,
|
||||
(id) => !id.startsWith("%leaf"),
|
||||
{ unaliased: () => true, leafId },
|
||||
)!;
|
||||
expect(plan.values.get(write)!.leaves).toHaveLength(1);
|
||||
expect([...plan.locals.keys()].some((id) => id.startsWith("%leaf"))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -30,6 +31,326 @@ export interface SpeculativeLoopPlan {
|
||||
* than through its condition, `break` or a failed check: no calls, no
|
||||
* throwing operations, no labeled jumps and no `return`. */
|
||||
observerFree: boolean;
|
||||
/** The writes that speculate a rewritten value (rewriteSpeculable). */
|
||||
values: Map<IrStmt, SpeculativeValue>;
|
||||
}
|
||||
|
||||
/** A write's value as speculation evaluates it: calls of small pure number
|
||||
* functions expanded in place, and other number leaves (`leaves`) replaced
|
||||
* by references to fresh locals. The fast copy evaluates each leaf once,
|
||||
* stores it to its local and checks it as an integer; a failed check
|
||||
* resumes at a copy of the write that reads the locals, so a leaf is never
|
||||
* evaluated twice. */
|
||||
export interface SpeculativeValue {
|
||||
value: IrExpr;
|
||||
leaves: { localId: string; expr: IrExpr; ref: IrExpr }[];
|
||||
}
|
||||
|
||||
/** What the planner may assume about the code around a loop. */
|
||||
export interface SpeculationContext {
|
||||
/** The parameters and the returned expression of `callee` when it is a
|
||||
* small pure number function (inlinableNumberFunction). */
|
||||
inlinable?: (callee: string) => InlinableFunction | undefined;
|
||||
/** A local that no call can write: not captured, boxed or global. */
|
||||
unaliased?: (localId: string) => boolean;
|
||||
/** A fresh local id for a hoisted leaf. */
|
||||
leafId?: () => string;
|
||||
}
|
||||
|
||||
export interface InlinableFunction {
|
||||
params: readonly string[];
|
||||
body: IrExpr;
|
||||
}
|
||||
|
||||
/** `s` (a speculated write) writing `value` instead. */
|
||||
export function withSpeculativeValue(s: IrStmt, value: IrExpr): IrStmt {
|
||||
switch (s.kind) {
|
||||
case "assign":
|
||||
return { ...s, value };
|
||||
case "varDecl":
|
||||
return { ...s, init: value };
|
||||
case "exprStmt":
|
||||
if (s.expr.kind === "assignExpr") return { ...s, expr: { ...s.expr, value } };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
throw new InternalCompilerError(`llvm emitter bug: speculated ${s.kind} with a rewritten value`);
|
||||
}
|
||||
|
||||
/** Calls nest at most this deep in one expansion. */
|
||||
const MAX_INLINE_DEPTH = 4;
|
||||
/** An expansion larger than this stays a call. */
|
||||
const MAX_INLINE_NODES = 64;
|
||||
|
||||
/** A function that only returns arithmetic over its number parameters:
|
||||
* `function trim(v, n) { return v - Math.floor(v / Math.pow(2, n)) * Math.pow(2, n); }`.
|
||||
* Its call evaluates nothing but its arguments, so a speculated write may
|
||||
* expand it in place. A call in its body must be of another such function
|
||||
* (`resolve`): any other call could change what the expansion reads. */
|
||||
export function inlinableNumberFunction(
|
||||
fn: IrFunction,
|
||||
resolve?: (callee: string) => InlinableFunction | undefined,
|
||||
): InlinableFunction | undefined {
|
||||
if (
|
||||
fn.async ||
|
||||
fn.generator ||
|
||||
(fn.captures?.length ?? 0) > 0 ||
|
||||
(fn.classCaptures?.length ?? 0) > 0 ||
|
||||
fn.returnType.kind !== "f64" ||
|
||||
fn.params.some((p) => p.type.kind !== "f64" && p.type.kind !== "union") ||
|
||||
fn.body.length !== 1 ||
|
||||
fn.body[0]!.kind !== "return" ||
|
||||
fn.body[0]!.value === undefined
|
||||
)
|
||||
return undefined;
|
||||
const params = fn.params.map((p) => p.localId);
|
||||
const known = new Set(params);
|
||||
let nodes = 0;
|
||||
// A parameter of a union type (`number | undefined`) is read only
|
||||
// through its tag test and narrowing, which a wrapped argument folds.
|
||||
const union = (e: IrExpr): boolean => e.kind === "varRef" && known.has(e.localId);
|
||||
const pure = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_INLINE_NODES) return false;
|
||||
if (e.kind === "boolLit") return true;
|
||||
if (e.kind === "unionIsTag") return union(e.value);
|
||||
if (e.kind === "unionNarrow") return e.type.kind === "f64" && union(e.value);
|
||||
if (e.type.kind !== "f64") return false;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return known.has(e.localId);
|
||||
case "ternary":
|
||||
return pure(e.cond) && pure(e.then) && pure(e.else_);
|
||||
case "bin":
|
||||
return (INTEGER_OPS.has(e.op) || e.op === "/") && pure(e.left) && pure(e.right);
|
||||
case "unary":
|
||||
return e.op === "-" && pure(e.operand);
|
||||
case "libCall":
|
||||
return (e.fn === "math.floor" || e.fn === "math.pow") && e.args.every(pure);
|
||||
case "call":
|
||||
return resolve?.(e.callee) !== undefined && e.args.every(pure);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const body = fn.body[0]!.value!;
|
||||
return pure(body) ? { params, body } : undefined;
|
||||
}
|
||||
|
||||
/** `e` with each parameter reference replaced by its argument. A tag test
|
||||
* or narrowing of a wrapped argument folds, and so does a ternary over a
|
||||
* folded test. */
|
||||
function substitute(e: IrExpr, args: ReadonlyMap<string, IrExpr>): IrExpr {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
return args.get(e.localId) ?? e;
|
||||
case "unionIsTag": {
|
||||
const value = substitute(e.value, args);
|
||||
if (value.kind !== "unionWrap" || value.unionId !== e.unionId) return { ...e, value };
|
||||
return {
|
||||
kind: "boolLit",
|
||||
value: (value.tag === e.tag) !== (e.negated === true),
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
case "unionNarrow": {
|
||||
const value = substitute(e.value, args);
|
||||
return value.kind === "unionWrap" && value.unionId === e.unionId && value.tag === e.tag
|
||||
? value.value
|
||||
: { ...e, value };
|
||||
}
|
||||
case "ternary": {
|
||||
const cond = substitute(e.cond, args);
|
||||
if (cond.kind === "boolLit") return substitute(cond.value ? e.then : e.else_, args);
|
||||
return { ...e, cond, then: substitute(e.then, args), else_: substitute(e.else_, args) };
|
||||
}
|
||||
case "bin":
|
||||
return { ...e, left: substitute(e.left, args), right: substitute(e.right, args) };
|
||||
case "unary":
|
||||
return { ...e, operand: substitute(e.operand, args) };
|
||||
case "libCall":
|
||||
case "call":
|
||||
return { ...e, args: e.args.map((a) => substitute(a, args)) };
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
/** k when `value` is exactly 2^k with k in [0, 52], else null.
|
||||
* Math.log2 rounds: 2^52 + 1 gives 52, so the power is compared back. */
|
||||
function exactPowerOfTwo(value: number): number | null {
|
||||
if (!(value >= 1)) return null;
|
||||
const k = Math.round(Math.log2(value));
|
||||
return k >= 0 && k <= 52 && 2 ** k === value ? k : null;
|
||||
}
|
||||
|
||||
/** The same speculated operand written twice: a local, a literal, or a
|
||||
* power of two of the same exponent. */
|
||||
function sameOperand(a: IrExpr, b: IrExpr): boolean {
|
||||
if (a.kind === "varRef") return b.kind === "varRef" && a.localId === b.localId;
|
||||
if (a.kind === "numLit") return b.kind === "numLit" && Object.is(a.value, b.value);
|
||||
return (
|
||||
a.kind === "libCall" &&
|
||||
b.kind === "libCall" &&
|
||||
a.fn === "math.pow" &&
|
||||
b.fn === "math.pow" &&
|
||||
powerOfTwo(a) &&
|
||||
powerOfTwo(b) &&
|
||||
sameOperand(a.args[1]!, b.args[1]!)
|
||||
);
|
||||
}
|
||||
|
||||
/** `a - Math.floor(a / p) * p` (either factor order) with p a power of two
|
||||
* and `a` a local or literal: the floor-mod of `a` by p. */
|
||||
function floorModPowerOfTwo(e: IrExpr): { value: IrExpr; power: IrExpr } | null {
|
||||
if (e.kind !== "bin" || e.op !== "-" || e.right.kind !== "bin" || e.right.op !== "*") return null;
|
||||
const { left, right } = e.right;
|
||||
const [floor, power] =
|
||||
left.kind === "libCall" && left.fn === "math.floor" ? [left, right] : [right, left];
|
||||
if (floor.kind !== "libCall" || floor.fn !== "math.floor" || floor.args.length !== 1) return null;
|
||||
const quotient = floor.args[0]!;
|
||||
if (quotient.kind !== "bin" || quotient.op !== "/") return null;
|
||||
if (!powerOfTwo(power) || !sameOperand(quotient.right, power)) return null;
|
||||
if (e.left.kind !== "varRef" && e.left.kind !== "numLit") return null;
|
||||
return sameOperand(quotient.left, e.left) ? { value: e.left, power } : null;
|
||||
}
|
||||
|
||||
/** Whether `e` is a power of two speculation can shift by:
|
||||
* `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */
|
||||
function powerOfTwo(e: IrExpr): boolean {
|
||||
if (e.kind === "numLit") return exactPowerOfTwo(e.value) !== null;
|
||||
return (
|
||||
e.kind === "libCall" &&
|
||||
e.fn === "math.pow" &&
|
||||
e.args.length === 2 &&
|
||||
e.args[0]!.kind === "numLit" &&
|
||||
e.args[0]!.value === 2
|
||||
);
|
||||
}
|
||||
|
||||
/** Typed-array element kinds whose elements are integers. */
|
||||
const INTEGER_ELEMENTS = new Set(["u8", "u8c", "i8", "u16", "i16", "u32", "i32"]);
|
||||
|
||||
/** A typed-array element read of an integer kind with no effect: an
|
||||
* out-of-range read gives NaN, which fails its check. Float elements would
|
||||
* only bail. */
|
||||
function isSpeculativeByteRead(e: IrExpr): boolean {
|
||||
return (
|
||||
e.kind === "bytesIntrinsic" &&
|
||||
e.method === "get" &&
|
||||
e.type.kind === "f64" &&
|
||||
e.receiver.type.kind === "bytes" &&
|
||||
INTEGER_ELEMENTS.has(e.receiver.type.elem) &&
|
||||
e.args.length === 1 &&
|
||||
isEffectFree(e.receiver) &&
|
||||
isEffectFree(e.args[0]!)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether evaluating `e` assigns no binding: no `++`/`--`, no assignment
|
||||
* expression, and no statement inside it. */
|
||||
function writesNoLocal(e: IrExpr): boolean {
|
||||
return everyExpr(e, {
|
||||
expr: (node) => node.kind !== "incDec" && node.kind !== "assignExpr",
|
||||
stmt: () => false,
|
||||
});
|
||||
}
|
||||
|
||||
/** Rewrite a write's value that isSpeculableValue refuses into one it can
|
||||
* speculate (SpeculativeValue), or null. Expansion and the new operations
|
||||
* (`Math.pow(2, n)`, `Math.floor(a / 2^n)`, byte reads) evaluate nothing
|
||||
* but numbers, so the fast copy may evaluate them in any order. Hoisted
|
||||
* leaves run in the write's evaluation order before the arithmetic; that
|
||||
* reorders nothing only when the write reads no heap and every local it
|
||||
* reads is one no call can write. */
|
||||
export function rewriteSpeculable(
|
||||
value: IrExpr,
|
||||
context: SpeculationContext,
|
||||
): SpeculativeValue | null {
|
||||
const leaves: SpeculativeValue["leaves"] = [];
|
||||
let reads = false;
|
||||
const locals: string[] = [];
|
||||
const rewrite = (e: IrExpr, depth: number): IrExpr | null => {
|
||||
if (e.type.kind !== "f64") return null;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return Number.isSafeInteger(e.value) && !Object.is(e.value, -0) ? e : null;
|
||||
case "varRef":
|
||||
locals.push(e.localId);
|
||||
return e;
|
||||
case "bin": {
|
||||
if (!ARITHMETIC_OPS.has(e.op)) break;
|
||||
const left = rewrite(e.left, depth);
|
||||
const right = left && rewrite(e.right, depth);
|
||||
return left && right ? { ...e, left, right } : null;
|
||||
}
|
||||
case "unary": {
|
||||
if (e.op !== "-") break;
|
||||
const operand = rewrite(e.operand, depth);
|
||||
return operand ? { ...e, operand } : null;
|
||||
}
|
||||
case "libCall": {
|
||||
if (powerOfTwo(e) && e.kind === "libCall") {
|
||||
const exponent = rewrite(e.args[1]!, depth);
|
||||
return exponent ? { ...e, args: [e.args[0]!, exponent] } : null;
|
||||
}
|
||||
const quotient = e.args[0];
|
||||
if (
|
||||
e.fn === "math.floor" &&
|
||||
e.args.length === 1 &&
|
||||
quotient?.kind === "bin" &&
|
||||
quotient.op === "/" &&
|
||||
powerOfTwo(quotient.right)
|
||||
) {
|
||||
const left = rewrite(quotient.left, depth);
|
||||
const right = left && rewrite(quotient.right, depth);
|
||||
return left && right ? { ...e, args: [{ ...quotient, left, right }] } : null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "call": {
|
||||
const fn = depth < MAX_INLINE_DEPTH ? context.inlinable?.(e.callee) : undefined;
|
||||
// Arguments are evaluated before the body: an expansion may read
|
||||
// them in another order, or twice, only when they are pure.
|
||||
const plain = (a: IrExpr): boolean =>
|
||||
isSpeculableValue(a, false) ||
|
||||
(a.kind === "unionWrap" && isSpeculableValue(a.value, false));
|
||||
if (fn !== undefined && fn.params.length === e.args.length && e.args.every(plain)) {
|
||||
const args = new Map(fn.params.map((id, i) => [id, e.args[i]!]));
|
||||
return rewrite(substitute(fn.body, args), depth + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) {
|
||||
reads = true;
|
||||
return e;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Only calls of program functions are hoisted: other leaves (a string's
|
||||
// length, a Map read) are cheap where they stand, and would turn every
|
||||
// loop that adds one into a second copy.
|
||||
if (context.leafId === undefined || e.kind !== "call") return null;
|
||||
// Hoisting runs the arguments before the rest of the write: one that
|
||||
// assigns a local would move that write ahead of the write's earlier
|
||||
// reads of it.
|
||||
if (!e.args.every(writesNoLocal)) return null;
|
||||
const ref: IrExpr = { kind: "varRef", localId: context.leafId(), type: e.type, loc: e.loc };
|
||||
leaves.push({ localId: (ref as { localId: string }).localId, expr: e, ref });
|
||||
return ref;
|
||||
};
|
||||
const rewritten = rewrite(value, 0);
|
||||
if (rewritten === null) return null;
|
||||
if (leaves.length > 0) {
|
||||
const leafIds = new Set(leaves.map((leaf) => leaf.localId));
|
||||
if (reads || !locals.every((id) => leafIds.has(id) || context.unaliased?.(id) === true))
|
||||
return null;
|
||||
}
|
||||
return { value: rewritten, leaves };
|
||||
}
|
||||
|
||||
type SpeculativeLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
|
||||
@@ -170,13 +491,16 @@ function isEffectFree(e: IrExpr): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** The speculative reads of a speculable value in evaluation order. */
|
||||
/** The speculative reads of a speculable value in evaluation order, those
|
||||
* of a rewritten value (rewriteSpeculable) included. */
|
||||
export function speculativeReads(e: IrExpr, out: IrExpr[] = []): IrExpr[] {
|
||||
if (e.kind === "bin" && ARITHMETIC_OPS.has(e.op)) {
|
||||
if (e.kind === "bin" && (ARITHMETIC_OPS.has(e.op) || e.op === "/")) {
|
||||
speculativeReads(e.left, out);
|
||||
speculativeReads(e.right, out);
|
||||
} else if (e.kind === "unary" && e.op === "-") speculativeReads(e.operand, out);
|
||||
else if (isSpeculativeRead(e)) out.push(e);
|
||||
else if (e.kind === "libCall" && (e.fn === "math.floor" || e.fn === "math.pow"))
|
||||
for (const arg of e.args) speculativeReads(arg, out);
|
||||
else if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) out.push(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -195,6 +519,8 @@ function hasProduct(e: IrExpr): boolean {
|
||||
if (e.kind === "bin")
|
||||
return e.op === "*" || e.op === "%" || hasProduct(e.left) || hasProduct(e.right);
|
||||
if (e.kind === "unary") return hasProduct(e.operand);
|
||||
// Math.floor(a / 2^n) and Math.pow(2, n) replace a division and a call.
|
||||
if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -202,6 +528,7 @@ function hasArithmetic(e: IrExpr): boolean {
|
||||
if (e.kind === "bin")
|
||||
return ARITHMETIC_OPS.has(e.op) || hasArithmetic(e.left) || hasArithmetic(e.right);
|
||||
if (e.kind === "unary") return e.op === "-" || hasArithmetic(e.operand);
|
||||
if (e.kind === "libCall") return e.fn === "math.floor" || e.fn === "math.pow";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -236,8 +563,11 @@ export function planSpeculativeLoop(
|
||||
integral: (localId: string) => boolean = () => false,
|
||||
proven: (e: IrExpr) => boolean = () => false,
|
||||
invariantEligible: (localId: string) => boolean = eligible,
|
||||
context: SpeculationContext = {},
|
||||
): SpeculativeLoopPlan | null {
|
||||
const writes = new Map<string, IrStmt[]>();
|
||||
const values = new Map<IrStmt, SpeculativeValue>();
|
||||
const leafIds = new Set<string>();
|
||||
const unspeculable = new Set<string>();
|
||||
const reads = new Set<string>();
|
||||
// Locals with at least one read the static analysis does not prove.
|
||||
@@ -263,11 +593,9 @@ export function planSpeculativeLoop(
|
||||
// make the body's work integer.
|
||||
let inCondition = false;
|
||||
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64"))
|
||||
observerFree = false;
|
||||
// What the integer writes read and consume. A rewritten value is noted
|
||||
// here alone: its original, which `expr` walks, has its effects.
|
||||
const note = (e: IrExpr): void => {
|
||||
if (e.kind === "varRef") {
|
||||
reads.add(e.localId);
|
||||
if (!proven(e)) unproven.add(e.localId);
|
||||
@@ -281,6 +609,20 @@ export function planSpeculativeLoop(
|
||||
} else if (e.kind === "arrayGet" || e.kind === "arrayHas" || e.kind === "arrayState")
|
||||
consume(e.index);
|
||||
else if (e.kind === "bytesIntrinsic" || e.kind === "strIntrinsic") e.args.forEach(consume);
|
||||
};
|
||||
// An expanded call is emitted in the fast copy: it counts toward the
|
||||
// duplicated code.
|
||||
const noteAll = (e: IrExpr): boolean => {
|
||||
nodes++;
|
||||
note(e);
|
||||
return everyExprChild(e, noteAll, () => true);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (!OBSERVER_FREE_EXPRESSIONS.has(e.kind) || (e.kind === "bin" && e.left.type.kind !== "f64"))
|
||||
observerFree = false;
|
||||
note(e);
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
// Statements inside expressions (sequence expressions) run mid-expression,
|
||||
@@ -307,7 +649,11 @@ export function planSpeculativeLoop(
|
||||
if (s.kind === "arraySet" || s.kind === "bytesSet") consume(s.index);
|
||||
const target = speculativeWriteTarget(s);
|
||||
if (target) {
|
||||
if (target.value === null || isSpeculableValue(target.value)) {
|
||||
const rewritten =
|
||||
target.value === null || isSpeculableValue(target.value)
|
||||
? null
|
||||
: rewriteSpeculable(target.value, context);
|
||||
if (target.value === null || isSpeculableValue(target.value) || rewritten !== null) {
|
||||
const list = writes.get(target.localId) ?? [];
|
||||
list.push(s);
|
||||
writes.set(target.localId, list);
|
||||
@@ -315,6 +661,11 @@ export function planSpeculativeLoop(
|
||||
reads.add(target.localId);
|
||||
if (s.kind !== "exprStmt" || !proven(s.expr)) unproven.add(target.localId);
|
||||
} else expr(target.value);
|
||||
if (rewritten !== null) {
|
||||
values.set(s, rewritten);
|
||||
for (const leaf of rewritten.leaves) leafIds.add(leaf.localId);
|
||||
noteAll(rewritten.value);
|
||||
}
|
||||
return !rejected;
|
||||
}
|
||||
unspeculable.add(target.localId);
|
||||
@@ -341,25 +692,28 @@ export function planSpeculativeLoop(
|
||||
// write runs: speculated itself, already an integer, or loop-invariant
|
||||
// (checked on entry). A value computed from doubles the loop writes
|
||||
// would only fail its check on every entry.
|
||||
const valueOf = (s: IrStmt): IrExpr | null =>
|
||||
values.get(s)?.value ?? speculativeWriteTarget(s)!.value;
|
||||
const grounded = (e: IrExpr): boolean => {
|
||||
if (e.kind === "varRef") {
|
||||
const id = e.localId;
|
||||
return (
|
||||
leafIds.has(id) ||
|
||||
proven(e) ||
|
||||
locals.has(id) ||
|
||||
integral(id) ||
|
||||
(!writes.has(id) && !unspeculable.has(id) && invariantEligible(id))
|
||||
);
|
||||
}
|
||||
if (isSpeculativeRead(e)) return true;
|
||||
if (isSpeculativeRead(e) || isSpeculativeByteRead(e)) return true;
|
||||
return everyExprChild(e, grounded, () => false);
|
||||
};
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of [...locals.keys()]) {
|
||||
for (const s of writes.get(id)!) {
|
||||
const target = speculativeWriteTarget(s)!;
|
||||
if (target.value !== null && !grounded(target.value)) {
|
||||
const value = valueOf(s);
|
||||
if (value !== null && !grounded(value)) {
|
||||
locals.delete(id);
|
||||
changed = true;
|
||||
break;
|
||||
@@ -376,6 +730,7 @@ export function planSpeculativeLoop(
|
||||
if (e.kind === "varRef") {
|
||||
const id = e.localId;
|
||||
if (
|
||||
!leafIds.has(id) &&
|
||||
!proven(e) &&
|
||||
!locals.has(id) &&
|
||||
!writes.has(id) &&
|
||||
@@ -388,11 +743,11 @@ export function planSpeculativeLoop(
|
||||
return everyExprChild(e, invariant, () => true);
|
||||
};
|
||||
for (const s of speculated) {
|
||||
const target = speculativeWriteTarget(s)!;
|
||||
if (target.value === null) arithmetic = true;
|
||||
const value = valueOf(s);
|
||||
if (value === null) arithmetic = true;
|
||||
else {
|
||||
if (hasArithmetic(target.value)) arithmetic = true;
|
||||
invariant(target.value);
|
||||
if (hasArithmetic(value)) arithmetic = true;
|
||||
invariant(value);
|
||||
}
|
||||
}
|
||||
if (!arithmetic) return null;
|
||||
@@ -401,13 +756,12 @@ export function planSpeculativeLoop(
|
||||
[...locals.keys()].some((id) => multiplied.has(id)) ||
|
||||
[...speculated].some((s) => {
|
||||
const target = speculativeWriteTarget(s)!;
|
||||
return (
|
||||
target.value !== null &&
|
||||
(hasProduct(target.value) || isVariableStep(target.localId, target.value))
|
||||
);
|
||||
const value = valueOf(s);
|
||||
return value !== null && (hasProduct(value) || isVariableStep(target.localId, value));
|
||||
});
|
||||
if (!heavy) return null;
|
||||
return { locals, writes: speculated, observerFree };
|
||||
for (const s of values.keys()) if (!speculated.has(s)) values.delete(s);
|
||||
return { locals, writes: speculated, observerFree, values };
|
||||
}
|
||||
|
||||
/** A proven integer loop. The bindings integer-ranges.ts bounds over a for
|
||||
@@ -627,6 +981,37 @@ export function emitSpeculativeInteger(
|
||||
}
|
||||
case "bin": {
|
||||
if (e.op !== "+" && e.op !== "-" && e.op !== "*" && e.op !== "%") break;
|
||||
// The floor-mod of a safe integer by 2^n (n in [0, 52]) is its low n
|
||||
// bits, also for a negative value: in [0, 2^n), and never -0.
|
||||
const mod = floorModPowerOfTwo(e);
|
||||
if (mod) {
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral);
|
||||
const n = exponentOf(host, mod.power, bail, reads, integral);
|
||||
const mask = B.tmp(),
|
||||
low = B.tmp();
|
||||
B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`);
|
||||
B.line(`${low} = and i64 ${value}, ${mask}`);
|
||||
return low;
|
||||
}
|
||||
// A product with 2^n shifts: it is safe iff the other factor's
|
||||
// magnitude is at most (2^53 - 1) >> n, and a zero product of a safe
|
||||
// integer and a positive power is +0.
|
||||
if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) {
|
||||
const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left];
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral);
|
||||
const n = exponentOf(host, power, bail, reads, integral);
|
||||
const limit = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
outside = B.tmp(),
|
||||
product = B.tmp();
|
||||
B.line(`${limit} = ashr i64 ${SAFE}, ${n}`);
|
||||
host.declare("declare i64 @llvm.abs.i64(i64, i1)");
|
||||
B.line(`${magnitude} = call i64 @llvm.abs.i64(i64 ${value}, i1 true)`);
|
||||
B.line(`${outside} = icmp ugt i64 ${magnitude}, ${limit}`);
|
||||
bail(outside);
|
||||
B.line(`${product} = shl nsw i64 ${value}, ${n}`);
|
||||
return product;
|
||||
}
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral);
|
||||
const result = B.tmp();
|
||||
@@ -682,6 +1067,19 @@ export function emitSpeculativeInteger(
|
||||
bail(negativeZero);
|
||||
return result;
|
||||
}
|
||||
case "libCall": {
|
||||
// A rewritten value's Math.pow(2, n) and Math.floor(a / 2^n)
|
||||
// (rewriteSpeculable): with n an integer in [0, 52], 2^n is exact and
|
||||
// so is a / 2^n for a safe integer a, whose floor the arithmetic shift
|
||||
// computes, also for negative a. No result is -0.
|
||||
if (e.fn === "math.pow") return shiftOne(host, exponentOf(host, e, bail, reads, integral));
|
||||
if (e.fn !== "math.floor" || e.args[0]?.kind !== "bin") break;
|
||||
const a = emitSpeculativeInteger(host, e.args[0].left, bail, reads, integral);
|
||||
const n = exponentOf(host, e.args[0].right, bail, reads, integral);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = ashr i64 ${a}, ${n}`);
|
||||
return result;
|
||||
}
|
||||
case "unary": {
|
||||
if (e.op !== "-") break;
|
||||
const a = emitSpeculativeInteger(host, e.operand, bail, reads, integral);
|
||||
@@ -699,6 +1097,35 @@ export function emitSpeculativeInteger(
|
||||
return speculativeLeaf(host, reads.get(e) ?? host.emitExpr(e), e, bail);
|
||||
}
|
||||
|
||||
/** The exponent of a power of two (Math.pow(2, n) or a literal 2^k) as an
|
||||
* i64 in [0, 52]; other exponents bail. */
|
||||
function exponentOf(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr,
|
||||
bail: (condition: string) => void,
|
||||
reads: ReadonlyMap<IrExpr, LlValue>,
|
||||
integral: boolean,
|
||||
): string {
|
||||
if (e.kind === "numLit") {
|
||||
const k = exactPowerOfTwo(e.value);
|
||||
if (k === null) throw new InternalCompilerError("llvm emitter bug: speculative power of two");
|
||||
return String(k);
|
||||
}
|
||||
if (e.kind !== "libCall")
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative power of two");
|
||||
const n = emitSpeculativeInteger(host, e.args[1]!, bail, reads, integral);
|
||||
const outside = host.B.tmp();
|
||||
host.B.line(`${outside} = icmp ugt i64 ${n}, 52`);
|
||||
bail(outside);
|
||||
return n;
|
||||
}
|
||||
|
||||
function shiftOne(host: LlvmEmitterContext, n: string): string {
|
||||
const result = host.B.tmp();
|
||||
host.B.line(`${result} = shl i64 1, ${n}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The safe i64 of an evaluated number: its proven integer, or a checked
|
||||
* conversion. */
|
||||
function speculativeLeaf(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { splitLlvmLibraryProgram, splitLlvmProgram } from "./split.js";
|
||||
import { splitLlvmLibraryProgram, splitLlvmProgram, splitLlvmProgramFile } from "./split.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { VOID } from "../../ir/ir.js";
|
||||
|
||||
@@ -61,17 +61,17 @@ test("splits generated LLVM functions and promotes only private cross-shard defi
|
||||
expect(actual!.promotedSymbols).not.toContain("public_entry");
|
||||
expect(actual!.publicSymbols).toEqual(["public_value", "public_entry"]);
|
||||
expect(actual!.shards[0]!.name).toBe("program-globals.ll");
|
||||
expect(actual!.shards[0]!.source).toContain("@hidden_value = hidden global i64 7");
|
||||
expect(actual!.shards[0]!.source()).toContain("@hidden_value = hidden global i64 7");
|
||||
expect(
|
||||
actual!.shards
|
||||
.slice(1)
|
||||
.every((shard) => shard.source.includes("@hidden_value = external hidden global i64")),
|
||||
.every((shard) => shard.source().includes("@hidden_value = external hidden global i64")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
actual!.shards.some(
|
||||
(shard) =>
|
||||
!shard.source.includes("define hidden i64 @right() #0") &&
|
||||
shard.source.includes("declare hidden i64 @right() #0"),
|
||||
!shard.source().includes("define hidden i64 @right() #0") &&
|
||||
shard.source().includes("declare hidden i64 @right() #0"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -82,9 +82,115 @@ test("thread-local definitions and declarations preserve their storage model acr
|
||||
"@hidden_value = internal thread_local global i64 7",
|
||||
);
|
||||
const split = splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })!;
|
||||
expect(split.shards[0]!.source).toContain("@hidden_value = hidden thread_local global i64 7");
|
||||
expect(split.shards[0]!.source()).toContain("@hidden_value = hidden thread_local global i64 7");
|
||||
for (const shard of split.shards.slice(1)) {
|
||||
expect(shard.source).toContain("@hidden_value = external hidden thread_local global i64");
|
||||
expect(shard.source()).toContain("@hidden_value = external hidden thread_local global i64");
|
||||
}
|
||||
});
|
||||
|
||||
const PADDING = Array.from(
|
||||
{ length: 80 },
|
||||
(_, i) =>
|
||||
`define internal i64 @pad_${i}() #0 {\nentry:\n ; ${"x".repeat(2048)}\n ret i64 ${i}\n}\n`,
|
||||
).join("\n");
|
||||
|
||||
/** SAMPLE with more globals after @public_value and enough functions to
|
||||
* split into several shards. */
|
||||
function withGlobals(globals: string, tail = ""): string {
|
||||
return `${SAMPLE.replace(
|
||||
"@public_value = constant i64 11",
|
||||
`@public_value = constant i64 11\n${globals}`,
|
||||
).replace("define i64 @public_entry", `${PADDING}\ndefine i64 @public_entry`)}${tail}`;
|
||||
}
|
||||
|
||||
/** Writes each shard and compiles it with clang. */
|
||||
async function compileShards(
|
||||
shards: readonly { name: string; source: () => string }[],
|
||||
): Promise<void> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-shards-"));
|
||||
scratch.push(dir);
|
||||
for (const shard of shards) {
|
||||
const source = join(dir, shard.name);
|
||||
await writeFile(source, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", `${source}.o`]);
|
||||
}
|
||||
}
|
||||
|
||||
test("private unnamed_addr lookup tables are promoted like internal globals", async () => {
|
||||
const split = splitLlvmProgram(
|
||||
withGlobals('@table = private unnamed_addr constant [2 x i8] c"\\01\\02"'),
|
||||
{ minimumBytes: 0, targetBytes: 64 * 1024 },
|
||||
)!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain('@table = hidden unnamed_addr constant [2 x i8] c"\\01\\02"');
|
||||
for (const shard of functions)
|
||||
expect(shard.source()).toContain("@table = external hidden constant [2 x i8]");
|
||||
expect(split.promotedSymbols).toContain("table");
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("aggregate initializers over several lines stay whole in the globals shard", async () => {
|
||||
// The regex global's comment holds unbalanced brackets.
|
||||
const split = splitLlvmProgram(
|
||||
withGlobals(`@regex = internal global { ptr, i64 } { ptr @left, i64 0 } ; /(a|[b/
|
||||
@vt = internal constant [2 x { i64, ptr }] [
|
||||
{ i64, ptr } { i64 1, ptr @left },
|
||||
{ i64, ptr } { i64 2, ptr @right }
|
||||
]`),
|
||||
{ minimumBytes: 0, targetBytes: 64 * 1024 },
|
||||
)!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain(
|
||||
"@vt = hidden constant [2 x { i64, ptr }] [\n { i64, ptr } { i64 1, ptr @left },\n { i64, ptr } { i64 2, ptr @right }\n]",
|
||||
);
|
||||
for (const shard of functions) {
|
||||
expect(shard.source()).toContain("@vt = external hidden constant [2 x { i64, ptr }]");
|
||||
expect(shard.source()).not.toContain("{ i64, ptr } { i64 1");
|
||||
}
|
||||
expect(split.promotedSymbols).toEqual(expect.arrayContaining(["regex", "vt"]));
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("globals after the attributes and metadata join the globals shard", async () => {
|
||||
const split = splitLlvmProgram(withGlobals("", "@flag = internal constant i1 true\n"), {
|
||||
minimumBytes: 0,
|
||||
targetBytes: 64 * 1024,
|
||||
})!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain("@flag = hidden constant i1 true");
|
||||
for (const shard of functions)
|
||||
expect(shard.source()).toContain("@flag = external hidden constant i1");
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("a module split from its file has the shards of the same module split in memory", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-file-"));
|
||||
scratch.push(dir);
|
||||
const options = { minimumBytes: 0, targetBytes: 64 * 1024 };
|
||||
const storage = withGlobals(
|
||||
`@table = private unnamed_addr constant [2 x i8] c"\\01\\02"
|
||||
@vt = internal constant [2 x { i64, ptr }] [
|
||||
{ i64, ptr } { i64 1, ptr @left },
|
||||
{ i64, ptr } { i64 2, ptr @right }
|
||||
]`,
|
||||
"@flag = internal constant i1 true\n",
|
||||
);
|
||||
for (const [name, source] of [
|
||||
["sample.ll", withGlobals("")],
|
||||
["storage.ll", storage],
|
||||
] as const) {
|
||||
const path = join(dir, name);
|
||||
await writeFile(path, source);
|
||||
const fromFile = splitLlvmProgramFile(path, options)!;
|
||||
expect(fromFile).not.toBeNull();
|
||||
const materialize = (split: NonNullable<typeof fromFile>) => ({
|
||||
...split,
|
||||
shards: split.shards.map((shard) => ({ name: shard.name, source: shard.source() })),
|
||||
});
|
||||
expect(materialize(fromFile)).toEqual(materialize(splitLlvmProgram(source, options)!));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +233,7 @@ entry:
|
||||
for (const shard of split.shards) {
|
||||
const path = join(dir, shard.name);
|
||||
const object = `${path}.o`;
|
||||
await writeFile(path, shard.source);
|
||||
await writeFile(path, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-O2", "-c", path, "-o", object]);
|
||||
objects.push(object);
|
||||
}
|
||||
@@ -169,10 +275,10 @@ test("shared metadata bounds shard growth while preserving every definition", ()
|
||||
expect(split).not.toBeNull();
|
||||
expect(split!.shards.length).toBeGreaterThan(1);
|
||||
expect(
|
||||
split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source), 0),
|
||||
split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source()), 0),
|
||||
).toBeLessThanOrEqual(2 * Buffer.byteLength(source));
|
||||
const definitions = split!.shards.flatMap((shard) =>
|
||||
[...shard.source.matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]),
|
||||
[...shard.source().matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]),
|
||||
);
|
||||
expect(definitions.sort()).toEqual([...names, "left", "right", "public_entry"].sort());
|
||||
expect(split!.publicSymbols).toEqual(["public_value", "public_entry"]);
|
||||
@@ -207,9 +313,9 @@ test("debug metadata stays on definitions when splitting LLVM modules", async ()
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-split-"));
|
||||
scratch.push(dir);
|
||||
for (const shard of split!.shards) {
|
||||
expect(shard.source).not.toMatch(/^declare.*!dbg/m);
|
||||
expect(shard.source()).not.toMatch(/^declare.*!dbg/m);
|
||||
const path = join(dir, shard.name);
|
||||
await writeFile(path, shard.source);
|
||||
await writeFile(path, shard.source());
|
||||
const result = spawnSync("clang", ["-Wno-override-module", "-c", path, "-o", `${path}.o`], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
@@ -250,7 +356,7 @@ test("every shard compiles and its merged object exposes only canonical public d
|
||||
for (const shard of split.shards) {
|
||||
const source = join(dir, shard.name);
|
||||
const object = `${source}.o`;
|
||||
await writeFile(source, shard.source);
|
||||
await writeFile(source, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", object]);
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
* canonical TU normally — cache optimization must never become correctness.
|
||||
*/
|
||||
|
||||
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
||||
|
||||
export interface LlvmProgramShard {
|
||||
/** Stable source identity used in object cache keys and diagnostics. */
|
||||
name: string;
|
||||
source: string;
|
||||
/** Assembles the shard's LLVM text. A module split from a file reads its
|
||||
* function bodies back from there on each call, so only the shards being
|
||||
* written are in memory. */
|
||||
source: () => string;
|
||||
}
|
||||
|
||||
export interface LlvmProgramSplit {
|
||||
@@ -54,12 +59,20 @@ const LIBRARY_TARGET_BYTES = 768 * 1024;
|
||||
const LIBRARY_MINIMUM_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
interface FunctionDef {
|
||||
source: string;
|
||||
/** UTF-8 length of the definition's text. */
|
||||
bytes: number;
|
||||
/** The definition's text: its header (promoted to hidden linkage when
|
||||
* internal), body and closing brace. A definition left in a file reads
|
||||
* its body through `read`. */
|
||||
text: (read: RangeReader) => string;
|
||||
declaration: string;
|
||||
symbol: string;
|
||||
promoted: boolean;
|
||||
}
|
||||
|
||||
/** Reads the text between two byte offsets of the module file. */
|
||||
type RangeReader = (start: number, end: number) => string;
|
||||
|
||||
interface GlobalDef {
|
||||
line: string;
|
||||
declaration: string | null;
|
||||
@@ -72,50 +85,68 @@ function symbolOf(spelling: string): string | null {
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function functionDefAt(
|
||||
lines: readonly string[],
|
||||
start: number,
|
||||
): { def: FunctionDef; end: number } | null {
|
||||
const header = lines[start]!;
|
||||
/** A definition's header line: the line its shard compiles, the
|
||||
* declaration every other shard carries, and its linkage. */
|
||||
function functionHeader(
|
||||
header: string,
|
||||
): { line: string; declaration: string; symbol: string; promoted: boolean } | null {
|
||||
const match =
|
||||
/^define\s+(internal\s+)?(.+?)\s+(@[-$._A-Za-z][-$.\w]*)\((.*)\)(.*)\{(?:\s*;.*)?$/.exec(
|
||||
header,
|
||||
);
|
||||
if (!match) return null;
|
||||
let end = start + 1;
|
||||
while (end < lines.length && lines[end] !== "}") end++;
|
||||
if (end >= lines.length) return null;
|
||||
const symbol = symbolOf(match[3]!);
|
||||
if (symbol === null) return null;
|
||||
const promoted = match[1] !== undefined;
|
||||
const linkage = promoted ? "hidden " : "";
|
||||
const definitionHeader = promoted
|
||||
? header.replace(/^define internal /, "define hidden ")
|
||||
: header;
|
||||
const source = [definitionHeader, ...lines.slice(start + 1, end + 1)].join("\n");
|
||||
const line = promoted ? header.replace(/^define internal /, "define hidden ") : header;
|
||||
// Parameter names are legal on declarations. Preserve the emitted text so
|
||||
// attributes, zeroext, and varargs stay byte-exact.
|
||||
// A definition's DISubprogram cannot be attached to a declaration in a
|
||||
// different shard. Its owning definition retains the original metadata.
|
||||
const attrs = match[5]!.replace(/\s+!dbg !\d+/g, "");
|
||||
const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${attrs}`.trimEnd();
|
||||
return { def: { source, declaration, symbol, promoted }, end };
|
||||
return { line, declaration, symbol, promoted };
|
||||
}
|
||||
|
||||
function functionDefAt(
|
||||
lines: readonly string[],
|
||||
start: number,
|
||||
): { def: FunctionDef; end: number } | null {
|
||||
const header = functionHeader(lines[start]!);
|
||||
if (header === null) return null;
|
||||
let end = start + 1;
|
||||
while (end < lines.length && lines[end] !== "}") end++;
|
||||
if (end >= lines.length) return null;
|
||||
const source = [header.line, ...lines.slice(start + 1, end + 1)].join("\n");
|
||||
return {
|
||||
def: {
|
||||
bytes: Buffer.byteLength(source),
|
||||
text: () => source,
|
||||
declaration: header.declaration,
|
||||
symbol: header.symbol,
|
||||
promoted: header.promoted,
|
||||
},
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
function globalDef(line: string): GlobalDef | null {
|
||||
const match =
|
||||
/^(@[-$._A-Za-z][-$.\w]*)\s+=\s+(internal\s+)?(thread_local(?:\([^)]*\))?\s+)?(global|constant)\s+(.+)$/.exec(
|
||||
/^(@[-$._A-Za-z][-$.\w]*)\s+=\s+((?:internal|private)\s+)?(thread_local(?:\([^)]*\))?\s+)?((?:local_)?unnamed_addr\s+)?(global|constant)\s+([\s\S]+)$/.exec(
|
||||
line,
|
||||
);
|
||||
if (!match) return null;
|
||||
const symbol = symbolOf(match[1]!);
|
||||
if (symbol === null) return null;
|
||||
// Internal and private definitions (such as private unnamed_addr lookup
|
||||
// tables) are local to their module: shards reach them as hidden ones.
|
||||
const promoted = match[2] !== undefined;
|
||||
const threadLocal = match[3] === undefined ? "" : `${match[3].trim()} `;
|
||||
// The declaration needs only the complete type. Emitted initializers begin
|
||||
// after it; the generated LLVM subset uses first-class scalars/pointers or
|
||||
// balanced aggregate types, so one small scanner is sufficient.
|
||||
const rest = match[5]!;
|
||||
const rest = match[6]!;
|
||||
let depth = 0;
|
||||
let quote = false;
|
||||
let typeEnd = -1;
|
||||
@@ -133,13 +164,30 @@ function globalDef(line: string): GlobalDef | null {
|
||||
if (typeEnd < 0) return null;
|
||||
const type = rest.slice(0, typeEnd);
|
||||
return {
|
||||
line: promoted ? line.replace(`${match[1]} = internal `, `${match[1]} = hidden `) : line,
|
||||
declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[4]} ${type}`,
|
||||
line: promoted ? line.replace(`${match[1]} = ${match[2]}`, `${match[1]} = hidden `) : line,
|
||||
declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[5]} ${type}`,
|
||||
symbol,
|
||||
promoted,
|
||||
};
|
||||
}
|
||||
|
||||
/** The change in aggregate nesting depth over one line of LLVM text,
|
||||
* outside string constants and comments (a regex global's comment spells
|
||||
* its source). */
|
||||
function bracketDepth(line: string): number {
|
||||
let depth = 0;
|
||||
let quote = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i]!;
|
||||
if (ch === '"') quote = !quote;
|
||||
if (quote) continue;
|
||||
if (ch === ";") break;
|
||||
if (ch === "{" || ch === "[" || ch === "<" || ch === "(") depth++;
|
||||
else if (ch === "}" || ch === "]" || ch === ">" || ch === ")") depth--;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
function stableBucket(symbol: string, count: number): number {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < symbol.length; i++) {
|
||||
@@ -154,7 +202,7 @@ function chunksOf(
|
||||
targetBytes: number,
|
||||
maximumBuckets: number,
|
||||
): { bucket: number; functions: FunctionDef[] }[] {
|
||||
const totalBytes = functions.reduce((sum, fn) => sum + Buffer.byteLength(fn.source) + 2, 0);
|
||||
const totalBytes = functions.reduce((sum, fn) => sum + fn.bytes + 2, 0);
|
||||
// A power-of-two bucket count changes only when the program crosses a wide
|
||||
// size band. Ordinary edits therefore keep every function in the same
|
||||
// symbol-hash bucket; ceil(total/target) would reshuffle the whole program
|
||||
@@ -168,6 +216,74 @@ function chunksOf(
|
||||
);
|
||||
}
|
||||
|
||||
/** A module's top-level forms, as the splitter classifies them. */
|
||||
interface ModuleParts {
|
||||
preamble: string[];
|
||||
globals: GlobalDef[];
|
||||
functions: FunctionDef[];
|
||||
trailer: string[];
|
||||
}
|
||||
|
||||
/** Classifies a module one line at a time. `define` lines go to
|
||||
* `onFunction`, which takes the following lines through the definition's
|
||||
* closing brace; every other line is offered to `line`. */
|
||||
class ModuleReader {
|
||||
readonly parts: ModuleParts = { preamble: [], globals: [], functions: [], trailer: [] };
|
||||
private sawDefinition = false;
|
||||
private inTrailer = false;
|
||||
/** A global definition whose aggregate initializer continues over the
|
||||
* following lines (vtable arrays list one element per line). */
|
||||
private pendingGlobal: { text: string; depth: number } | null = null;
|
||||
failed = false;
|
||||
|
||||
/** Whether a definition may start here (none may follow the trailer). */
|
||||
startFunction(): boolean {
|
||||
if (this.inTrailer || this.pendingGlobal !== null) this.failed = true;
|
||||
this.sawDefinition = true;
|
||||
return !this.failed;
|
||||
}
|
||||
|
||||
line(line: string): void {
|
||||
if (this.pendingGlobal !== null) {
|
||||
this.pendingGlobal.text += "\n" + line;
|
||||
this.pendingGlobal.depth += bracketDepth(line);
|
||||
if (this.pendingGlobal.depth <= 0) {
|
||||
const { text } = this.pendingGlobal;
|
||||
this.pendingGlobal = null;
|
||||
this.global(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("@") && line.includes(" = ")) {
|
||||
const depth = bracketDepth(line);
|
||||
if (depth > 0) this.pendingGlobal = { text: line, depth };
|
||||
else this.global(line);
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("attributes ") || line.startsWith("!")) this.inTrailer = true;
|
||||
if (this.inTrailer) this.parts.trailer.push(line);
|
||||
else if (!this.sawDefinition || line.trim() !== "") this.parts.preamble.push(line);
|
||||
}
|
||||
|
||||
/** Globals may follow the attributes and metadata (the feature flags the
|
||||
* link-features step appends); they are module storage like any other. */
|
||||
private global(definition: string): void {
|
||||
const parsed = globalDef(definition);
|
||||
if (parsed === null) {
|
||||
// External declarations are already valid in every shard.
|
||||
if (/^@.+\s+=\s+external\s+/.test(definition)) this.parts.preamble.push(definition);
|
||||
else this.failed = true;
|
||||
} else {
|
||||
this.parts.globals.push(parsed);
|
||||
this.sawDefinition = true;
|
||||
}
|
||||
}
|
||||
|
||||
finish(): ModuleParts | null {
|
||||
return this.failed || this.pendingGlobal !== null ? null : this.parts;
|
||||
}
|
||||
}
|
||||
|
||||
export function splitLlvmProgram(
|
||||
source: string,
|
||||
options: LlvmProgramSplitOptions = {},
|
||||
@@ -175,42 +291,129 @@ export function splitLlvmProgram(
|
||||
const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES;
|
||||
const sourceBytes = Buffer.byteLength(source);
|
||||
if (sourceBytes < minimumBytes) return null;
|
||||
const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES);
|
||||
const lines = source.split("\n");
|
||||
const preamble: string[] = [];
|
||||
const globals: GlobalDef[] = [];
|
||||
const functions: FunctionDef[] = [];
|
||||
const trailer: string[] = [];
|
||||
let sawDefinition = false;
|
||||
let inTrailer = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const reader = new ModuleReader();
|
||||
for (let i = 0; i < lines.length && !reader.failed; i++) {
|
||||
const line = lines[i]!;
|
||||
if (line.startsWith("define ")) {
|
||||
if (inTrailer) return null;
|
||||
if (!reader.startFunction()) break;
|
||||
const parsed = functionDefAt(lines, i);
|
||||
if (parsed === null) return null;
|
||||
functions.push(parsed.def);
|
||||
sawDefinition = true;
|
||||
reader.parts.functions.push(parsed.def);
|
||||
i = parsed.end;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("@") && line.includes(" = ")) {
|
||||
if (inTrailer) return null;
|
||||
const parsed = globalDef(line);
|
||||
if (parsed === null) {
|
||||
// External declarations are already valid in every shard.
|
||||
if (/^@.+\s+=\s+external\s+/.test(line)) preamble.push(line);
|
||||
else return null;
|
||||
} else {
|
||||
globals.push(parsed);
|
||||
sawDefinition = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("attributes ") || line.startsWith("!")) inTrailer = true;
|
||||
if (inTrailer) trailer.push(line);
|
||||
else if (!sawDefinition || line.trim() !== "") preamble.push(line);
|
||||
} else reader.line(line);
|
||||
}
|
||||
const parts = reader.finish();
|
||||
return parts === null ? null : shardModule(parts, sourceBytes, options, null);
|
||||
}
|
||||
|
||||
/** The read size of splitLlvmProgramFile's scan. */
|
||||
const FILE_CHUNK_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/** splitLlvmProgram over LLVM text already written to `path`, for modules
|
||||
* too large to hold in memory more than once (or past the longest string the
|
||||
* engine allows). Function bodies stay in the file: each shard's source
|
||||
* reads its bodies when it is read, so only the shards being written at a
|
||||
* time are in memory. */
|
||||
export function splitLlvmProgramFile(
|
||||
path: string,
|
||||
options: LlvmProgramSplitOptions = {},
|
||||
): LlvmProgramSplit | null {
|
||||
const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES;
|
||||
const sourceBytes = statSync(path).size;
|
||||
if (sourceBytes < minimumBytes) return null;
|
||||
const reader = new ModuleReader();
|
||||
// The definition being read: its header and where its body starts.
|
||||
let current: { header: NonNullable<ReturnType<typeof functionHeader>>; body: number } | null =
|
||||
null;
|
||||
const onLine = (bytes: Buffer, start: number, end: number, offset: number): void => {
|
||||
if (current !== null) {
|
||||
// Only the closing brace ends a definition; its body is not decoded.
|
||||
if (end - start === 1 && bytes[start] === 0x7d) {
|
||||
const { header, body } = current;
|
||||
const bodyEnd = offset + 1;
|
||||
reader.parts.functions.push({
|
||||
bytes: Buffer.byteLength(header.line) + 1 + (bodyEnd - body),
|
||||
text: (read) => `${header.line}\n${read(body, bodyEnd)}`,
|
||||
declaration: header.declaration,
|
||||
symbol: header.symbol,
|
||||
promoted: header.promoted,
|
||||
});
|
||||
current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const line = bytes.toString("utf8", start, end);
|
||||
if (line.startsWith("define ")) {
|
||||
const header = functionHeader(line);
|
||||
if (header === null || !reader.startFunction()) reader.failed = true;
|
||||
else current = { header, body: offset + (end - start) + 1 };
|
||||
} else reader.line(line);
|
||||
};
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
let carry: Buffer = Buffer.alloc(0);
|
||||
let carryOffset = 0;
|
||||
for (let position = 0; position < sourceBytes && !reader.failed;) {
|
||||
const chunk = Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES, sourceBytes - position));
|
||||
const read = readSync(fd, chunk, 0, chunk.length, position);
|
||||
if (read === 0) break;
|
||||
const data =
|
||||
carry.length === 0
|
||||
? chunk.subarray(0, read)
|
||||
: Buffer.concat([carry, chunk.subarray(0, read)]);
|
||||
const dataOffset = carry.length === 0 ? position : carryOffset;
|
||||
position += read;
|
||||
let lineStart = 0;
|
||||
for (let newline = data.indexOf(10); newline >= 0; newline = data.indexOf(10, lineStart)) {
|
||||
onLine(data, lineStart, newline, dataOffset + lineStart);
|
||||
lineStart = newline + 1;
|
||||
}
|
||||
carry = Buffer.from(data.subarray(lineStart));
|
||||
carryOffset = dataOffset + lineStart;
|
||||
}
|
||||
// The text after the last newline is a line too (empty when the file
|
||||
// ends with one), as String.prototype.split answers it.
|
||||
if (!reader.failed) onLine(carry, 0, carry.length, carryOffset);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
if (current !== null) return null;
|
||||
const parts = reader.finish();
|
||||
return parts === null ? null : shardModule(parts, sourceBytes, options, path);
|
||||
}
|
||||
|
||||
/** Runs `use` with a reader of `path`'s byte ranges (or of nothing, for a
|
||||
* module held in memory). */
|
||||
function withRangeReader<T>(path: string | null, use: (read: RangeReader) => T): T {
|
||||
if (path === null)
|
||||
return use(() => {
|
||||
throw new Error("an in-memory LLVM module has no file to read");
|
||||
});
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
return use((start, end) => {
|
||||
const bytes = Buffer.allocUnsafe(end - start);
|
||||
for (let done = 0; done < bytes.length;) {
|
||||
const read = readSync(fd, bytes, done, bytes.length - done, start + done);
|
||||
if (read === 0) throw new Error(`${path} ended before byte ${end}`);
|
||||
done += read;
|
||||
}
|
||||
return bytes.toString("utf8");
|
||||
});
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function shardModule(
|
||||
parts: ModuleParts,
|
||||
sourceBytes: number,
|
||||
options: LlvmProgramSplitOptions,
|
||||
path: string | null,
|
||||
): LlvmProgramSplit | null {
|
||||
const { preamble, globals, functions, trailer } = parts;
|
||||
const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES);
|
||||
if (functions.length < 2) return null;
|
||||
const globalDecls = globals.flatMap((global) =>
|
||||
global.declaration === null ? [] : [global.declaration],
|
||||
@@ -230,16 +433,22 @@ export function splitLlvmProgram(
|
||||
const chunks = chunksOf(functions, targetBytes, 2 ** Math.floor(Math.log2(bucketLimit)));
|
||||
if (chunks.length < 2) return null;
|
||||
const tail = trailer.length === 0 ? "" : `\n${trailer.join("\n")}`;
|
||||
const functionShards = chunks.map(({ bucket, functions: chunk }) => {
|
||||
// A shard's source is assembled when it is read; a module split from a
|
||||
// file reads its function bodies from there each time.
|
||||
const functionShards = chunks.map(({ bucket, functions: chunk }): LlvmProgramShard => {
|
||||
const owned = new Set(chunk);
|
||||
const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration);
|
||||
return {
|
||||
name: `program-f${bucket.toString().padStart(3, "0")}.ll`,
|
||||
source: [
|
||||
[...preamble, ...globalDecls, ...declarations, ""].join("\n"),
|
||||
...chunk.map((fn) => fn.source),
|
||||
tail,
|
||||
].join("\n"),
|
||||
source: () => {
|
||||
const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration);
|
||||
return withRangeReader(path, (read) =>
|
||||
[
|
||||
[...preamble, ...globalDecls, ...declarations, ""].join("\n"),
|
||||
...chunk.map((fn) => fn.text(read)),
|
||||
tail,
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
// Global initializer changes are common (literal text, generated tables)
|
||||
@@ -247,15 +456,15 @@ export function splitLlvmProgram(
|
||||
// all module storage in one dedicated shard that declares every function.
|
||||
const globalShard: LlvmProgramShard = {
|
||||
name: "program-globals.ll",
|
||||
source: [
|
||||
[...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"),
|
||||
...globals.map((global) => global.line),
|
||||
tail,
|
||||
].join("\n"),
|
||||
source: () =>
|
||||
[
|
||||
[...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"),
|
||||
...globals.map((global) => global.line),
|
||||
tail,
|
||||
].join("\n"),
|
||||
};
|
||||
const shards = [globalShard, ...functionShards];
|
||||
return {
|
||||
shards,
|
||||
shards: [globalShard, ...functionShards],
|
||||
promotedSymbols: [
|
||||
...globals.filter((global) => global.promoted).map((global) => global.symbol),
|
||||
...functions.filter((fn) => fn.promoted).map((fn) => fn.symbol),
|
||||
|
||||
@@ -286,8 +286,9 @@ function bump(s: State): State { return { ...s, count: s.count + 1 }; }
|
||||
export function tokens(n: number, w: string): number {
|
||||
const t = { kind: "id", text: w, pos: n };
|
||||
const u = make("num", w, n);
|
||||
const [q, r] = divmod(n, 7);
|
||||
return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + q + r;
|
||||
// Read by index: a destructured literal result is inlined instead.
|
||||
const qr = divmod(n, 7);
|
||||
return t.pos + width(u) + width({ kind: w, text: w, pos: 0 }) + qr[0] + qr[1];
|
||||
}
|
||||
export function states(n: number, w: string): number {
|
||||
let s: State = { name: w, count: 0 };
|
||||
|
||||
@@ -24,6 +24,10 @@ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
|
||||
|
||||
export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)";
|
||||
export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)";
|
||||
/** Converts a string to UTF-16 units and publishes it as the scan receiver
|
||||
* (bytes-loop-versions.ts). Like the fallbacks below, it can evict an
|
||||
* index entry, so it carries no memory restriction. */
|
||||
export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units(ptr)";
|
||||
/** The runtime fallbacks are cold: both common cases read inline. They
|
||||
* carry no
|
||||
* memory restriction: a fallback can evict an index entry, which resets
|
||||
@@ -75,6 +79,19 @@ export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: st
|
||||
return code;
|
||||
}
|
||||
|
||||
/** Unit `index` (an i64, below their count) of a string's UTF-16 `units`
|
||||
* (ScrStrScan): one 16-bit load, as a double. */
|
||||
export function emitUnitCharCode(host: LlvmEmitterContext, units: string, index: string): string {
|
||||
const B = host.B;
|
||||
const pointer = B.tmp(),
|
||||
unit = B.tmp(),
|
||||
code = B.tmp();
|
||||
B.line(`${pointer} = getelementptr inbounds i16, ptr ${units}, i64 ${index}`);
|
||||
B.line(`${unit} = load i16, ptr ${pointer}`);
|
||||
B.line(`${code} = uitofp i16 ${unit} to double`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/** The one-character string of unit `index` (an i64) of a proven-ASCII
|
||||
* string `s` whose length covers it: the immortal `@sc_ascii_chars` entry
|
||||
* for its byte, as the inline charAt helpers answer. */
|
||||
@@ -114,6 +131,15 @@ function loadLength(sz: string): string[] {
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether emitted reads use the runtime's scan receiver: from ordinary
|
||||
* global storage, or, in worker executables, from the thread's static TLS
|
||||
* block (executableTls). */
|
||||
export function stringScanInline(host: LlvmEmitterContext): boolean {
|
||||
return (
|
||||
host.executableTls || (host.mod.workers !== true && host.mod.lib?.threadInstances !== true)
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime's ScrStrScan record and the declaration naming it. */
|
||||
export function stringScanType(sz: string): string {
|
||||
return `{ ptr, ptr, ${sz} }`;
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { analyzeCallLifetimes } from "./call-lifetimes.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
findLentStringParameters,
|
||||
findRecordFieldProducers,
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
} from "./string-slices.js";
|
||||
@@ -142,12 +153,12 @@ function lending(body: IrStmt[]): IrFunction {
|
||||
function scratch(fn: IrFunction, extra: IrFunction[] = []): Set<IrExpr> {
|
||||
const functions = new Map([fn, ...extra].map((f) => [f.name, f]));
|
||||
const lifetimes = analyzeCallLifetimes(functions);
|
||||
return findScratchStringSlices(
|
||||
fn,
|
||||
lifetimes,
|
||||
findScalarStringSlices(fn, lifetimes),
|
||||
findLentStringParameters(functions, lifetimes),
|
||||
);
|
||||
const strings = findLentStringParameters(functions, lifetimes);
|
||||
return findScratchStringSlices(fn, lifetimes, findScalarStringSlices(fn, lifetimes), {
|
||||
strings,
|
||||
records: findLentRecordFields(functions, lifetimes, strings),
|
||||
producers: findRecordFieldProducers(functions),
|
||||
});
|
||||
}
|
||||
test("slices read only by lending consumers use a stack buffer", () => {
|
||||
const inline = slice("source", 0, 3);
|
||||
@@ -242,3 +253,177 @@ test("helper parameters read only by lending consumers accept stack slices", ()
|
||||
const all = new Map([fn, ...functions].map((f) => [f.name, f]));
|
||||
expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]);
|
||||
});
|
||||
|
||||
const TOKEN: IrType = { kind: "record", shapeId: "token" };
|
||||
const record = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: TOKEN, loc });
|
||||
const textOf = (obj: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj,
|
||||
shapeId: "token",
|
||||
field: "text",
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
const lengthOf = (receiver: IrExpr): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const tokenLit = (text: IrExpr): IrExpr => ({
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "text", value: text },
|
||||
{ name: "pos", value: { kind: "numLit", value: 1, type: F64, loc } },
|
||||
],
|
||||
type: TOKEN,
|
||||
loc,
|
||||
});
|
||||
const keepIn = (value: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayLit", elems: [value], type: { kind: "array", elem: STRING }, loc },
|
||||
loc,
|
||||
});
|
||||
/** A function of one record parameter `t`. */
|
||||
function recordHelper(name: string, body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "t", name: "t", type: TOKEN }],
|
||||
locals: [{ id: "t", name: "t", type: TOKEN, mutable: false }],
|
||||
returnType: F64,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
/** `make(text)` returns `{ text, pos: 1 }`; `twice` also reads its parameter elsewhere. */
|
||||
function producerHelper(name: string, extra: IrStmt[] = []): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "text", name: "text", type: STRING }],
|
||||
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
|
||||
returnType: TOKEN,
|
||||
loc,
|
||||
body: [...extra, { kind: "return", value: tokenLit(ref("text")), loc }],
|
||||
};
|
||||
}
|
||||
function containing(init: IrExpr, declare: IrExpr, uses: IrStmt[]): IrFunction {
|
||||
const fn = lending([
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "varDecl", localId: "tok", init: declare, loc },
|
||||
...uses,
|
||||
]);
|
||||
fn.locals.push({ id: "tok", name: "tok", type: TOKEN, mutable: false });
|
||||
return fn;
|
||||
}
|
||||
const reads = recordHelper("reads", [
|
||||
{ kind: "return", value: lengthOf(textOf(record("t"))), loc },
|
||||
]);
|
||||
const keepsText = recordHelper("keepsText", [
|
||||
keepIn(textOf(record("t"))),
|
||||
{ kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc },
|
||||
]);
|
||||
const escapes = recordHelper("escapes", [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayLit", elems: [record("t")], type: { kind: "array", elem: TOKEN }, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "numLit", value: 0, type: F64, loc }, loc },
|
||||
]);
|
||||
const passCall = (callee: string, arg: IrExpr): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "call", callee, args: [arg], type: F64, loc },
|
||||
loc,
|
||||
});
|
||||
|
||||
test("a slice stored in a record that is only projected uses a stack buffer", () => {
|
||||
const helpers = [reads, keepsText, escapes, producerHelper("make")];
|
||||
for (const declare of [tokenLit(ref("word")), callOf("make", ref("word"))]) {
|
||||
if (declare.kind === "call") declare.type = TOKEN;
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = containing(init, declare, [
|
||||
{ kind: "exprStmt", expr: equals(textOf(record("tok"))), loc },
|
||||
passCall("reads", record("tok")),
|
||||
]);
|
||||
expect(scratch(fn, helpers).has(init)).toBe(true);
|
||||
}
|
||||
const all = new Map(helpers.map((f) => [f.name, f]));
|
||||
const lifetimes = analyzeCallLifetimes(all);
|
||||
const lent = findLentRecordFields(all, lifetimes, findLentStringParameters(all, lifetimes));
|
||||
expect(lent.get("reads")?.get(0)).toEqual(new Set());
|
||||
expect(lent.get("keepsText")?.get(0)).toEqual(new Set(["text"]));
|
||||
expect(lent.has("escapes")).toBe(false);
|
||||
expect(findRecordFieldProducers(all).get("make")).toEqual(new Map([[0, "text"]]));
|
||||
});
|
||||
|
||||
test.each([
|
||||
"kept-field",
|
||||
"returned",
|
||||
"kept-by-callee",
|
||||
"escapes-callee",
|
||||
"captured",
|
||||
"producer-reuse",
|
||||
"producer-capture",
|
||||
])("a slice stored in a record that may be kept (%s) stays owned", (reason) => {
|
||||
// The factory also stores a closure over its text parameter.
|
||||
const capturing = producerHelper("capturing");
|
||||
const literal = (capturing.body[0] as IrStmt & { kind: "return" }).value as IrExpr & {
|
||||
kind: "recordLit";
|
||||
};
|
||||
literal.fields.push({
|
||||
name: "get",
|
||||
value: { kind: "closure", fnName: "get", captures: ["text"], type: F64, loc },
|
||||
});
|
||||
const helpers = [
|
||||
reads,
|
||||
keepsText,
|
||||
escapes,
|
||||
producerHelper("make"),
|
||||
producerHelper("twice", [keepIn(ref("text"))]),
|
||||
capturing,
|
||||
];
|
||||
const init = slice("source", 1, 5);
|
||||
const declare =
|
||||
reason === "producer-reuse"
|
||||
? callOf("twice", ref("word"))
|
||||
: reason === "producer-capture"
|
||||
? callOf("capturing", ref("word"))
|
||||
: tokenLit(ref("word"));
|
||||
if (declare.kind === "call") declare.type = TOKEN;
|
||||
const uses: IrStmt[] = [{ kind: "exprStmt", expr: equals(textOf(record("tok"))), loc }];
|
||||
if (reason === "kept-field") uses.push(keepIn(textOf(record("tok"))));
|
||||
if (reason === "returned") uses.push({ kind: "return", value: record("tok"), loc });
|
||||
if (reason === "kept-by-callee") uses.push(passCall("keepsText", record("tok")));
|
||||
if (reason === "escapes-callee") uses.push(passCall("escapes", record("tok")));
|
||||
const fn = containing(init, declare, uses);
|
||||
if (reason === "captured") fn.locals.find((l) => l.id === "tok")!.boxed = true;
|
||||
expect(scratch(fn, helpers).has(init)).toBe(false);
|
||||
});
|
||||
|
||||
test("searches of string arrays lend their needle", () => {
|
||||
const keywords: IrExpr = {
|
||||
kind: "varRef",
|
||||
localId: "keywords",
|
||||
type: { kind: "array", elem: STRING },
|
||||
loc,
|
||||
};
|
||||
const classify = helper("classify", [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "includes",
|
||||
receiver: keywords,
|
||||
args: [ref("text")],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
const init = slice("source", 0, 3);
|
||||
const fn = lending([{ kind: "exprStmt", expr: callOf("classify", init), loc }]);
|
||||
expect(scratch(fn, [classify]).has(init)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse
|
||||
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
|
||||
type Slice = IrExpr & { kind: "strIntrinsic" };
|
||||
|
||||
@@ -66,6 +67,58 @@ export function findScalarStringSlices(
|
||||
return slices;
|
||||
}
|
||||
|
||||
/** scr_slice_unit: a slice()/substring() bound `v` over `length` units. A
|
||||
* non-negative bound clamps to the length; a negative one counts from the
|
||||
* end for slice (truncated toward zero) and is 0 for substring, as is NaN. */
|
||||
function emitSliceUnit(
|
||||
host: LlvmEmitterContext,
|
||||
v: string,
|
||||
length: string,
|
||||
substring: boolean,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const lengthNumber = B.tmp(),
|
||||
nonNegative = B.tmp(),
|
||||
past = B.tmp(),
|
||||
whole = B.tmp(),
|
||||
clamped = B.tmp(),
|
||||
result = B.tmp();
|
||||
B.line(`${lengthNumber} = ${sizeToDouble(size)} ${size} ${length} to double`);
|
||||
B.line(`${nonNegative} = fcmp oge double ${v}, 0.0`);
|
||||
B.line(`${past} = fcmp oge double ${v}, ${lengthNumber}`);
|
||||
// Frozen: the conversion is only used where the bound is in range.
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = fptosi double ${v} to i64`);
|
||||
B.line(`${whole} = freeze i64 ${raw}`);
|
||||
let unit = whole;
|
||||
if (size !== "i64") {
|
||||
unit = B.tmp();
|
||||
B.line(`${unit} = trunc i64 ${whole} to ${size}`);
|
||||
}
|
||||
B.line(`${clamped} = select i1 ${past}, ${size} ${length}, ${size} ${unit}`);
|
||||
if (substring) {
|
||||
B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} 0`);
|
||||
return result;
|
||||
}
|
||||
// Negative: (-length, -1] counts back from the end; (-1, 0) truncates to
|
||||
// 0, and at or below -length, and NaN, give 0.
|
||||
const negLength = B.tmp(),
|
||||
inside = B.tmp(),
|
||||
zero = B.tmp(),
|
||||
back = B.tmp(),
|
||||
counted = B.tmp(),
|
||||
negative = B.tmp();
|
||||
B.line(`${negLength} = fneg double ${lengthNumber}`);
|
||||
B.line(`${inside} = fcmp ogt double ${v}, ${negLength}`);
|
||||
B.line(`${zero} = icmp eq ${size} ${unit}, 0`);
|
||||
B.line(`${back} = add ${size} ${length}, ${unit}`);
|
||||
B.line(`${counted} = select i1 ${zero}, ${size} 0, ${size} ${back}`);
|
||||
B.line(`${negative} = select i1 ${inside}, ${size} ${counted}, ${size} 0`);
|
||||
B.line(`${result} = select i1 ${nonNegative}, ${size} ${clamped}, ${size} ${negative}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface StringSliceSnapshot {
|
||||
source: string;
|
||||
range: string;
|
||||
@@ -87,10 +140,55 @@ export function emitStringSliceSnapshot(
|
||||
`${range} = alloca { ${host.sizeType}, ${host.sizeType}, i32 }`,
|
||||
);
|
||||
B.line(`store ptr ${source.name}, ptr ${sourceSlot}`);
|
||||
const start = args[0]?.name ?? f64Lit(0),
|
||||
end = args[1]?.name ?? F64_INF;
|
||||
const substring = slice.method === "substring";
|
||||
// A proven-ASCII receiver's units are its bytes, so the range is the
|
||||
// runtime's clamped bounds over its byte length, and no boundary splits a
|
||||
// surrogate pair (scr_str_slice_range); other strings ask the runtime.
|
||||
const ascii = B.newLabel("slice.range.ascii"),
|
||||
runtime = B.newLabel("slice.range.runtime"),
|
||||
done = B.newLabel("slice.range.done");
|
||||
B.condBr(emitStringAsciiFlag(host, source.name), ascii, runtime);
|
||||
B.startBlock(ascii);
|
||||
const size = host.sizeType;
|
||||
const length = emitStringByteLength(host, source.name);
|
||||
let from = emitSliceUnit(host, start, length, substring),
|
||||
to = emitSliceUnit(host, end, length, substring);
|
||||
if (substring) {
|
||||
const swap = B.tmp(),
|
||||
low = B.tmp(),
|
||||
high = B.tmp();
|
||||
B.line(`${swap} = icmp ugt ${size} ${from}, ${to}`);
|
||||
B.line(`${low} = select i1 ${swap}, ${size} ${to}, ${size} ${from}`);
|
||||
B.line(`${high} = select i1 ${swap}, ${size} ${from}, ${size} ${to}`);
|
||||
from = low;
|
||||
to = high;
|
||||
}
|
||||
const nonEmpty = B.tmp(),
|
||||
difference = B.tmp(),
|
||||
units = B.tmp(),
|
||||
startPtr = B.tmp(),
|
||||
lengthPtr = B.tmp(),
|
||||
splitPtr = B.tmp();
|
||||
B.line(`${nonEmpty} = icmp ugt ${size} ${to}, ${from}`);
|
||||
B.line(`${difference} = sub ${size} ${to}, ${from}`);
|
||||
B.line(`${units} = select i1 ${nonEmpty}, ${size} ${difference}, ${size} 0`);
|
||||
const rangeType = `{ ${size}, ${size}, i32 }`;
|
||||
B.line(`${startPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 0`);
|
||||
B.line(`store ${size} ${from}, ptr ${startPtr}`);
|
||||
B.line(`${lengthPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 1`);
|
||||
B.line(`store ${size} ${units}, ptr ${lengthPtr}`);
|
||||
B.line(`${splitPtr} = getelementptr inbounds ${rangeType}, ptr ${range}, i32 0, i32 2`);
|
||||
B.line(`store i32 0, ptr ${splitPtr}`);
|
||||
B.br(done);
|
||||
B.startBlock(runtime);
|
||||
host.declare("declare void @scr_str_slice_range(ptr, double, double, i1 zeroext, ptr)");
|
||||
B.line(
|
||||
`call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`,
|
||||
`call void @scr_str_slice_range(ptr ${source.name}, double ${start}, double ${end}, i1 ${substring ? "true" : "false"}, ptr ${range})`,
|
||||
);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
host.toFrame(source);
|
||||
return { source: sourceSlot, range };
|
||||
}
|
||||
@@ -145,6 +243,10 @@ const LENDING_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"num.parseFloat",
|
||||
]);
|
||||
|
||||
/** Searches of string arrays compare the needle with the elements during
|
||||
* the call. */
|
||||
const LENDING_ARRAY_SEARCHES: ReadonlySet<string> = new Set(["includes", "indexOf", "lastIndexOf"]);
|
||||
|
||||
/** Typed string-keyed collection operations: reads probe with the key, and
|
||||
* an insertion stores scr_str_scratch_keep's copy (scr_map_append). */
|
||||
const LENDING_KEY_METHODS: ReadonlySet<string> = new Set(["get", "has", "set", "add", "delete"]);
|
||||
@@ -176,6 +278,12 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly
|
||||
return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value)
|
||||
? value.args.slice(0, 1)
|
||||
: [];
|
||||
case "arrIntrinsic":
|
||||
return LENDING_ARRAY_SEARCHES.has(value.method) &&
|
||||
value.receiver.type.kind === "array" &&
|
||||
value.receiver.type.elem.kind === "string"
|
||||
? value.args.slice(0, 1)
|
||||
: [];
|
||||
case "call": {
|
||||
const lent = parameters.get(value.callee);
|
||||
return lent ? value.args.filter((_, index) => lent.has(index)) : [];
|
||||
@@ -185,41 +293,123 @@ function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly
|
||||
}
|
||||
}
|
||||
|
||||
/** Record parameters, by function and index, that the body only projects:
|
||||
* it reads their fields and passes them only to such parameters. The set
|
||||
* lists the string fields some read keeps (reads outside a lending
|
||||
* position); every other string field is lent (findLentRecordFields). */
|
||||
export type LentRecordFields = ReadonlyMap<string, ReadonlyMap<number, ReadonlySet<string>>>;
|
||||
|
||||
/** Functions whose body only returns a record literal that stores some
|
||||
* parameters, each read once, directly as fields: by function, parameter
|
||||
* index to field (findRecordFieldProducers). A call stores those arguments
|
||||
* in the fields of its result and keeps them nowhere else. */
|
||||
export type RecordFieldProducers = ReadonlyMap<string, ReadonlyMap<number, string>>;
|
||||
|
||||
/** What lends a string: lent string parameters, lent record fields and
|
||||
* record field producers. */
|
||||
export interface LendingFacts {
|
||||
strings: LentStringParameters;
|
||||
records: LentRecordFields;
|
||||
producers: RecordFieldProducers;
|
||||
}
|
||||
|
||||
const NO_LENDING_FACTS: LendingFacts = {
|
||||
strings: new Map(),
|
||||
records: new Map(),
|
||||
producers: new Map(),
|
||||
};
|
||||
|
||||
interface LendingScan {
|
||||
/** Slice operands of lending consumers, excluding any node also reached
|
||||
* in an owning position. */
|
||||
scratch: Set<IrExpr>;
|
||||
/** Candidates read anywhere other than a lending position. */
|
||||
invalid: Set<string>;
|
||||
/** Direct callees, for the parameter fixpoint. */
|
||||
/** Record candidates read other than by a field read or a lent record
|
||||
* argument. */
|
||||
invalidRecords: Set<string>;
|
||||
/** String fields of record candidates read outside a lending position. */
|
||||
keptFields: Map<string, Set<string>>;
|
||||
/** Direct callees, for the parameter fixpoints. */
|
||||
callees: Set<string>;
|
||||
}
|
||||
|
||||
/** One pass over `fn`: which candidate bindings only lending consumers read
|
||||
* (a slice initializer of a candidate local is not itself a read), and
|
||||
* which slice operands those consumers lend. A switch discriminant is only
|
||||
* compared with the case tests. */
|
||||
* compared with the case tests. Record candidates (`records`) may only be
|
||||
* projected: their field reads are checked as reads of the field, and they
|
||||
* may be passed to lent record parameters. A record `container`
|
||||
* declaration stores candidates in fields of a record candidate; those
|
||||
* stores are not reads. */
|
||||
function scanLending(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
initializers: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters,
|
||||
facts: LendingFacts,
|
||||
records: ReadonlySet<string> = new Set(),
|
||||
containers: ReadonlyMap<string, RecordContainer> = new Map(),
|
||||
): LendingScan {
|
||||
const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() };
|
||||
const scan: LendingScan = {
|
||||
scratch: new Set(),
|
||||
invalid: new Set(),
|
||||
invalidRecords: new Set(),
|
||||
keptFields: new Map(),
|
||||
callees: new Set(),
|
||||
};
|
||||
const owned = new Set<IrExpr>();
|
||||
const keep = (record: string, field: string): void => {
|
||||
let fields = scan.keptFields.get(record);
|
||||
if (!fields) scan.keptFields.set(record, (fields = new Set()));
|
||||
fields.add(field);
|
||||
};
|
||||
const projected = (value: IrExpr): value is IrExpr & { kind: "recordGet" } =>
|
||||
value.kind === "recordGet" && value.obj.kind === "varRef" && records.has(value.obj.localId);
|
||||
function lent(value: IrExpr): boolean {
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) return true;
|
||||
if (projected(value)) return true;
|
||||
if (!isSlice(value)) return expr(value);
|
||||
scan.scratch.add(value);
|
||||
return everyExprChild(value, expr, stmt);
|
||||
}
|
||||
/** A call's arguments: lent string parameters lend, lent record
|
||||
* parameters project their record, and `stored` arguments (a producer's
|
||||
* fields in a container declaration) are not reads. */
|
||||
function callArgs(value: IrExpr & { kind: "call" }, stored: ReadonlySet<number> | null): void {
|
||||
scan.callees.add(value.callee);
|
||||
const strings = facts.strings.get(value.callee);
|
||||
const recordParams = facts.records.get(value.callee);
|
||||
value.args.forEach((arg, index) => {
|
||||
if (stored !== null && stored.has(index)) return;
|
||||
const kept = recordParams?.get(index);
|
||||
if (kept && arg.kind === "varRef" && records.has(arg.localId)) {
|
||||
for (const field of kept) keep(arg.localId, field);
|
||||
return;
|
||||
}
|
||||
if (strings?.has(index)) lent(arg);
|
||||
else expr(arg);
|
||||
});
|
||||
}
|
||||
function expr(value: IrExpr): boolean {
|
||||
if (isSlice(value)) owned.add(value);
|
||||
if (projected(value)) {
|
||||
if (value.type.kind === "string")
|
||||
keep((value.obj as { localId: string }).localId, value.field);
|
||||
return true;
|
||||
}
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId);
|
||||
if (value.kind === "varRef" && records.has(value.localId))
|
||||
scan.invalidRecords.add(value.localId);
|
||||
if (value.kind === "closure" || value.kind === "classRef")
|
||||
for (const id of value.captures ?? []) scan.invalid.add(id);
|
||||
if (value.kind === "call") scan.callees.add(value.callee);
|
||||
const operands = lentOperands(value, parameters);
|
||||
for (const id of value.captures ?? []) {
|
||||
scan.invalid.add(id);
|
||||
scan.invalidRecords.add(id);
|
||||
}
|
||||
if (value.kind === "call") {
|
||||
callArgs(value, null);
|
||||
return true;
|
||||
}
|
||||
const operands = lentOperands(value, facts.strings);
|
||||
if (operands.length === 0) return everyExprChild(value, expr, stmt);
|
||||
return everyExprChild(
|
||||
value,
|
||||
@@ -231,6 +421,20 @@ function scanLending(
|
||||
// A candidate's single declaration initializes it with its slice.
|
||||
if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId))
|
||||
return everyExprChild(value.init, expr, stmt);
|
||||
const container = value.kind === "varDecl" ? containers.get(value.localId) : undefined;
|
||||
if (container && value.kind === "varDecl" && value.init !== null) {
|
||||
const init = value.init;
|
||||
if (init.kind === "recordLit")
|
||||
for (const field of init.fields) {
|
||||
if (!container.stored.has(field.value)) expr(field.value);
|
||||
}
|
||||
else if (init.kind === "call")
|
||||
callArgs(
|
||||
init,
|
||||
new Set(init.args.flatMap((arg, i) => (container.stored.has(arg) ? [i] : []))),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (value.kind === "switch" && value.disc.type.kind === "string") {
|
||||
lent(value.disc);
|
||||
for (const entry of value.cases) {
|
||||
@@ -276,7 +480,7 @@ export function findLentStringParameters(
|
||||
const fn = functions.get(name);
|
||||
if (!indexes || !fn) continue;
|
||||
const ids = new Set([...indexes].map((index) => fn.params[index]!.localId));
|
||||
const scan = scanLending(fn, ids, new Map(), lent);
|
||||
const scan = scanLending(fn, ids, new Map(), { ...NO_LENDING_FACTS, strings: lent });
|
||||
for (const callee of scan.callees) {
|
||||
let set = callers.get(callee);
|
||||
if (!set) callers.set(callee, (set = new Set()));
|
||||
@@ -296,6 +500,151 @@ export function findLentStringParameters(
|
||||
return lent;
|
||||
}
|
||||
|
||||
/** Functions whose whole body is `return { ... }`, a record literal storing
|
||||
* some parameters, each read nowhere else, directly as fields (a factory
|
||||
* such as `token(kind, text, pos)`). A synchronous body only: the result
|
||||
* then holds those arguments, and nothing else does. */
|
||||
export function findRecordFieldProducers(
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
): Map<string, Map<number, string>> {
|
||||
const producers = new Map<string, Map<number, string>>();
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.async || fn.generator || fn.body.length !== 1) continue;
|
||||
const only = fn.body[0]!;
|
||||
if (only.kind !== "return" || only.value?.kind !== "recordLit") continue;
|
||||
const literal = only.value;
|
||||
const reads = new Map<string, number>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (value) => {
|
||||
if (value.kind === "varRef") reads.set(value.localId, (reads.get(value.localId) ?? 0) + 1);
|
||||
// A closure or class that captures a parameter keeps it too.
|
||||
if (value.kind === "closure" || value.kind === "classRef")
|
||||
for (const id of value.captures ?? []) reads.set(id, (reads.get(id) ?? 0) + 1);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
const fields = new Map<number, string>();
|
||||
fn.params.forEach((param, index) => {
|
||||
if (param.type.kind !== "string" || reads.get(param.localId) !== 1) return;
|
||||
const field = literal.fields.find(
|
||||
(entry) =>
|
||||
!entry.overflow && entry.value.kind === "varRef" && entry.value.localId === param.localId,
|
||||
);
|
||||
if (field) fields.set(index, field.name);
|
||||
});
|
||||
if (fields.size > 0) producers.set(fn.name, fields);
|
||||
}
|
||||
return producers;
|
||||
}
|
||||
|
||||
/** Record parameters that every body only projects (LentRecordFields), with
|
||||
* the string fields some read keeps. Start from every borrowed record
|
||||
* parameter of a synchronous function, keeping no field, and grow the kept
|
||||
* fields and drop escaping parameters until nothing changes; a record passed
|
||||
* to another lent record parameter keeps what that parameter keeps. */
|
||||
export function findLentRecordFields(
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
lifetimes: CallLifetimes,
|
||||
strings: LentStringParameters,
|
||||
): Map<string, Map<number, Set<string>>> {
|
||||
const lent = new Map<string, Map<number, Set<string>>>();
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.async || fn.generator) continue;
|
||||
const stable = lifetimes.borrowed.get(fn.name);
|
||||
const indexes = new Map<number, Set<string>>();
|
||||
fn.params.forEach((param, index) => {
|
||||
if (param.type.kind === "record" && stable?.has(index)) indexes.set(index, new Set());
|
||||
});
|
||||
if (indexes.size > 0) lent.set(fn.name, indexes);
|
||||
}
|
||||
const callers = new Map<string, Set<string>>();
|
||||
const pending = new Set(lent.keys());
|
||||
while (pending.size > 0) {
|
||||
const name = pending.values().next().value as string;
|
||||
pending.delete(name);
|
||||
const indexes = lent.get(name);
|
||||
const fn = functions.get(name);
|
||||
if (!indexes || !fn) continue;
|
||||
const ids = new Set([...indexes.keys()].map((index) => fn.params[index]!.localId));
|
||||
const scan = scanLending(
|
||||
fn,
|
||||
new Set(),
|
||||
new Map(),
|
||||
{ strings, records: lent, producers: new Map() },
|
||||
ids,
|
||||
);
|
||||
for (const callee of scan.callees) {
|
||||
let set = callers.get(callee);
|
||||
if (!set) callers.set(callee, (set = new Set()));
|
||||
set.add(name);
|
||||
}
|
||||
let changed = false;
|
||||
for (const [index, kept] of [...indexes]) {
|
||||
const id = fn.params[index]!.localId;
|
||||
if (scan.invalidRecords.has(id)) {
|
||||
indexes.delete(index);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const fields = scan.keptFields.get(id);
|
||||
if (fields)
|
||||
for (const field of fields) {
|
||||
if (kept.has(field)) continue;
|
||||
kept.add(field);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) continue;
|
||||
if (indexes.size === 0) lent.delete(name);
|
||||
const affected = callers.get(name);
|
||||
if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller);
|
||||
}
|
||||
return lent;
|
||||
}
|
||||
|
||||
/** A record local whose declaration stores slice locals directly in some of
|
||||
* its fields: a record literal, or a call to a record field producer. */
|
||||
interface RecordContainer {
|
||||
/** The stored candidate reads of the declaration, by field. */
|
||||
stored: Map<IrExpr, { field: string; slice: string }>;
|
||||
}
|
||||
|
||||
/** Record locals declared with slice locals in their fields. */
|
||||
function findRecordContainers(
|
||||
fn: IrFunction,
|
||||
bindings: ReadonlySet<string> | undefined,
|
||||
slices: ReadonlySet<string>,
|
||||
producers: RecordFieldProducers,
|
||||
): Map<string, RecordContainer> {
|
||||
const containers = new Map<string, RecordContainer>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (stmt.kind !== "varDecl" || stmt.init === null || !bindings?.has(stmt.localId))
|
||||
return true;
|
||||
if (stmt.init.type.kind !== "record") return true;
|
||||
const stored = new Map<IrExpr, { field: string; slice: string }>();
|
||||
const init = stmt.init;
|
||||
if (init.kind === "recordLit") {
|
||||
for (const field of init.fields)
|
||||
if (!field.overflow && field.value.kind === "varRef" && slices.has(field.value.localId))
|
||||
stored.set(field.value, { field: field.name, slice: field.value.localId });
|
||||
} else if (init.kind === "call") {
|
||||
const fields = producers.get(init.callee);
|
||||
init.args.forEach((arg, index) => {
|
||||
const field = fields?.get(index);
|
||||
if (field !== undefined && arg.kind === "varRef" && slices.has(arg.localId))
|
||||
stored.set(arg, { field, slice: arg.localId });
|
||||
});
|
||||
}
|
||||
if (stored.size > 0) containers.set(stmt.localId, { stored });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return containers;
|
||||
}
|
||||
|
||||
/** Slices whose every reader borrows them are written into a stack buffer
|
||||
* instead of a heap allocation (scr_str_slice_scratch): a slice operand of
|
||||
* a lending consumer, and the initializer of a stable local read only by
|
||||
@@ -304,12 +653,18 @@ export function findLentStringParameters(
|
||||
* non-ASCII results keep the ordinary owned string. Each site owns its
|
||||
* buffer: a site runs again only after its previous value is dead, since
|
||||
* nothing can keep it. Coroutine frames do not keep stack buffers across a
|
||||
* suspension, so they are left alone. */
|
||||
* suspension, so they are left alone.
|
||||
*
|
||||
* A slice local may also be stored in a field of a stable record local
|
||||
* declared with it (a record literal, or a record field producer's result):
|
||||
* the record is then only projected, and every read of that field lends.
|
||||
* The record is declared after the slice in its scope and is never kept, so
|
||||
* it is dead before the slice's site runs again. */
|
||||
export function findScratchStringSlices(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
scalar: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters = new Map(),
|
||||
facts: LendingFacts = NO_LENDING_FACTS,
|
||||
): Set<IrExpr> {
|
||||
if (fn.async || fn.generator) return new Set();
|
||||
const bindings = lifetimes.bindings.get(fn.name);
|
||||
@@ -328,7 +683,19 @@ export function findScratchStringSlices(
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const scan = scanLending(fn, new Set(locals.keys()), locals, parameters);
|
||||
const containers = findRecordContainers(fn, bindings, new Set(locals.keys()), facts.producers);
|
||||
const scan = scanLending(
|
||||
fn,
|
||||
new Set(locals.keys()),
|
||||
locals,
|
||||
facts,
|
||||
new Set(containers.keys()),
|
||||
containers,
|
||||
);
|
||||
for (const [record, container] of containers)
|
||||
for (const { field, slice } of container.stored.values())
|
||||
if (scan.invalidRecords.has(record) || scan.keptFields.get(record)?.has(field))
|
||||
scan.invalid.add(slice);
|
||||
for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init);
|
||||
return scan.scratch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import type { IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { unitSafeFunctions, unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
async function lower(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unit-versions-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const loopOf = (mod: IrModule, name: string): IrStmt & { kind: "for" } =>
|
||||
mod.functions.find((f) => f.name === name)!.body.find((s) => s.kind === "for") as IrStmt & {
|
||||
kind: "for";
|
||||
};
|
||||
|
||||
test("loops over a string's units call only functions that read no string", async () => {
|
||||
const mod = await lower(`
|
||||
function lower(c: number): boolean {
|
||||
return c >= 97 && c <= 122;
|
||||
}
|
||||
function word(c: number): boolean {
|
||||
return lower(c) || (c >= 48 && c <= 57) || Math.abs(c - 200) < 50;
|
||||
}
|
||||
function codeOf(t: string, i: number): number {
|
||||
return t.charCodeAt(i);
|
||||
}
|
||||
function viaCodeOf(t: string): number {
|
||||
return codeOf(t, 0);
|
||||
}
|
||||
function words(s: string): number {
|
||||
let n = 0;
|
||||
const starts = [0];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (word(s.charCodeAt(i))) n++;
|
||||
else starts.push(i);
|
||||
}
|
||||
return n + starts.length;
|
||||
}
|
||||
function pairs(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + codeOf(t, i);
|
||||
return n;
|
||||
}
|
||||
function both(s: string, t: string): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) n += s.charCodeAt(i) + t.charCodeAt(i);
|
||||
return n;
|
||||
}
|
||||
function concatenates(s: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) out += "-";
|
||||
return out;
|
||||
}
|
||||
console.log(words("ab c"), pairs("ab", "cd"), both("ab", "cd"), concatenates("a b"), viaCodeOf("x"));
|
||||
`);
|
||||
const safe = unitSafeFunctions(mod);
|
||||
expect(safe.has("lower")).toBe(true);
|
||||
expect(safe.has("word")).toBe(true);
|
||||
// A string read, directly or through a call, makes a function unsafe.
|
||||
expect(safe.has("codeOf")).toBe(false);
|
||||
expect(safe.has("viaCodeOf")).toBe(false);
|
||||
|
||||
const param = (fn: string, index = 0): string =>
|
||||
mod.functions.find((f) => f.name === fn)!.params[index]!.localId;
|
||||
// A unit-safe loop answers its receiver's length reads: here the
|
||||
// condition's `s.length`.
|
||||
const lengths = unitSafeLoop(loopOf(mod, "words"), param("words"), safe);
|
||||
expect(lengths?.size).toBe(1);
|
||||
expect([...lengths!].every((e) => e.kind === "strIntrinsic" && e.method === "length")).toBe(true);
|
||||
expect(unitSafeLoop(loopOf(mod, "pairs"), param("pairs"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "both"), param("both"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "concatenates"), param("concatenates"), safe)).toBeNull();
|
||||
});
|
||||
|
||||
test("loops that sort with a comparator or copy an untyped source get no units copy", async () => {
|
||||
// Either can run program code that indexes other strings, which evicts
|
||||
// the receiver's units while the loop reads them.
|
||||
const mod = await lower(`
|
||||
function sorts(s: string, values: Float64Array, compare: (a: number, b: number) => number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.sort(compare);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function copies(s: string, values: Float64Array, source: any): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.set(source);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function sortsNumbers(s: string, values: Float64Array): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
n += s.charCodeAt(i);
|
||||
if (i === 3) values.sort();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
const values = new Float64Array([2, 1]);
|
||||
console.log(sorts("abcdef", values, (a, b) => a - b), copies("abcdef", values, [4, 3]), sortsNumbers("abcdef", values));
|
||||
`);
|
||||
const safe = unitSafeFunctions(mod);
|
||||
const param = (fn: string): string =>
|
||||
mod.functions.find((f) => f.name === fn)!.params[0]!.localId;
|
||||
expect(unitSafeLoop(loopOf(mod, "sorts"), param("sorts"), safe)).toBeNull();
|
||||
expect(unitSafeLoop(loopOf(mod, "copies"), param("copies"), safe)).toBeNull();
|
||||
// A numeric sort calls no program code.
|
||||
expect(unitSafeLoop(loopOf(mod, "sortsNumbers"), param("sortsNumbers"), safe)).not.toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { IrBytesIntrinsicMethod, IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
|
||||
/* Loop versions over the UTF-16 units of a string that is not ASCII.
|
||||
*
|
||||
* The runtime publishes the receiver of an indexed scan with its UTF-16
|
||||
* units (ScrStrScan, string-reads.ts), and drops them when the string is
|
||||
* released or appended to in place, or when its index entry is evicted:
|
||||
* the runtime indexes another non-ASCII string. A loop version's string
|
||||
* receiver is a binding the loop does not write, which keeps it alive and
|
||||
* unappended for the whole loop. Its fast copy over units therefore only
|
||||
* needs a body that indexes no other string: the operations below, and
|
||||
* calls of functions made of the same operations without string reads
|
||||
* (`unitSafeFunctions`). */
|
||||
|
||||
const ARRAY_METHODS = new Set(["push", "pop", "length"]);
|
||||
|
||||
/* The typed-array and Buffer methods a units copy may run: those that
|
||||
* call no program code and read no checked-dynamic value or string. A
|
||||
* comparator sort (sortCompare) calls back into the program, and a copy
|
||||
* from a checked-dynamic source (setFromDyn) reads its elements through
|
||||
* getters; either can index another string, which evicts the receiver's
|
||||
* units while the loop reads them. */
|
||||
const UNIT_SAFE_BYTES_METHODS: ReadonlySet<IrBytesIntrinsicMethod> = new Set([
|
||||
"buffer",
|
||||
"length",
|
||||
"byteLength",
|
||||
"byteOffset",
|
||||
"get",
|
||||
"slice",
|
||||
"subarray",
|
||||
"copyWithin",
|
||||
"sortNumeric",
|
||||
"toReversed",
|
||||
"with",
|
||||
"toArray",
|
||||
"setFrom",
|
||||
"readNum",
|
||||
"writeNum",
|
||||
"readNumVar",
|
||||
"writeNumVar",
|
||||
"equals",
|
||||
"compareBuf",
|
||||
"indexOfNum",
|
||||
"lastIndexOfNum",
|
||||
"includesNum",
|
||||
"indexOfElem",
|
||||
"lastIndexOfElem",
|
||||
"includesElem",
|
||||
"fill",
|
||||
"fillNum",
|
||||
"fillElem",
|
||||
"copy",
|
||||
"swap16",
|
||||
"swap32",
|
||||
"swap64",
|
||||
"dataViewNew",
|
||||
"dvGetUint8",
|
||||
"dvGetInt8",
|
||||
"dvGetUint16",
|
||||
"dvGetInt16",
|
||||
"dvGetUint32",
|
||||
"dvGetInt32",
|
||||
"dvGetFloat16",
|
||||
"dvGetFloat32",
|
||||
"dvGetFloat64",
|
||||
"dvGetBigUint64Number",
|
||||
"dvGetBigInt64Number",
|
||||
"dvSetUint8",
|
||||
"dvSetInt8",
|
||||
"dvSetUint16",
|
||||
"dvSetInt16",
|
||||
"dvSetUint32",
|
||||
"dvSetInt32",
|
||||
"dvSetFloat16",
|
||||
"dvSetFloat32",
|
||||
"dvSetFloat64",
|
||||
]);
|
||||
|
||||
/** Whether `e` (or statement `s`) uses only operations that index no
|
||||
* string: numbers, booleans, locals, records and class fields, typed
|
||||
* arrays, number arrays and Math. `reads` admits the string reads a
|
||||
* version covers; `calls` the callees. */
|
||||
function unitSafe(
|
||||
node: { e: IrExpr } | { s: IrStmt },
|
||||
reads: (e: IrExpr & { kind: "strIntrinsic" }) => boolean,
|
||||
calls: (callee: string) => boolean,
|
||||
): boolean {
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
case "bin":
|
||||
case "unary":
|
||||
case "logical":
|
||||
case "ternary":
|
||||
case "toBool":
|
||||
case "seqExpr":
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
case "recordGet":
|
||||
case "fieldGet":
|
||||
break;
|
||||
case "bytesIntrinsic":
|
||||
if (!UNIT_SAFE_BYTES_METHODS.has(e.method)) return false;
|
||||
break;
|
||||
case "arrayGet":
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Growing or shrinking an array of numbers allocates; it reads no
|
||||
// string.
|
||||
const array = e.receiver.type;
|
||||
if (
|
||||
!ARRAY_METHODS.has(e.method) ||
|
||||
array.kind !== "array" ||
|
||||
(array.elem.kind !== "f64" && array.elem.kind !== "bool")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "libCall":
|
||||
if (!e.fn.startsWith("math.")) return false;
|
||||
break;
|
||||
case "call":
|
||||
if (!calls(e.callee)) return false;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (!reads(e)) return false;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
case "exprStmt":
|
||||
case "if":
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "break":
|
||||
case "continue":
|
||||
case "return":
|
||||
case "recordSet":
|
||||
case "fieldSet":
|
||||
case "bytesSet":
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
return "e" in node ? expr(node.e) : stmt(node.s);
|
||||
}
|
||||
|
||||
/** The module's functions a units version may call: synchronous
|
||||
* functions without captures whose bodies use only unit-safe operations,
|
||||
* read no string, and call only such functions. */
|
||||
export function unitSafeFunctions(mod: IrModule): ReadonlySet<string> {
|
||||
const candidates = new Map(
|
||||
mod.functions
|
||||
.filter((fn) => !fn.async && !fn.generator && !(fn.captures?.length ?? 0))
|
||||
.map((fn) => [fn.name, fn]),
|
||||
);
|
||||
const safe = new Set(candidates.keys());
|
||||
for (let settled = false; !settled;) {
|
||||
settled = true;
|
||||
for (const name of safe) {
|
||||
const fn = candidates.get(name)!;
|
||||
const ok = fn.body.every((s) =>
|
||||
unitSafe(
|
||||
{ s },
|
||||
() => false,
|
||||
(callee) => safe.has(callee),
|
||||
),
|
||||
);
|
||||
if (!ok) {
|
||||
safe.delete(name);
|
||||
settled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
/** The `length` reads of the binding `receiver` in a loop whose every
|
||||
* part is unit-safe, reading only charCodeAt and length of `receiver` and
|
||||
* calling only `safe` functions; null for any other loop. */
|
||||
export function unitSafeLoop(
|
||||
loop: IrStmt & { kind: "for" | "while" },
|
||||
receiver: string,
|
||||
safe: ReadonlySet<string>,
|
||||
): ReadonlySet<IrExpr> | null {
|
||||
const lengths = new Set<IrExpr>();
|
||||
const reads = (e: IrExpr & { kind: "strIntrinsic" }): boolean => {
|
||||
if (e.receiver.kind !== "varRef" || e.receiver.localId !== receiver) return false;
|
||||
if (e.method === "length") lengths.add(e);
|
||||
return e.method === "charCodeAt" || e.method === "length";
|
||||
};
|
||||
const calls = (callee: string): boolean => safe.has(callee);
|
||||
return unitSafe({ s: loop }, reads, calls) ? lengths : null;
|
||||
}
|
||||
@@ -1,14 +1,29 @@
|
||||
import type { IrModule, IrType } from "../../ir/ir.js";
|
||||
import { F64, type IrModule, type IrType, type IrUnionDef } from "../../ir/ir.js";
|
||||
|
||||
/** Test helper: a Set of each union keeps it a tagged `%ScrUnion` box (Set
|
||||
* elements are runtime-inspected boxes), so stack-box tests exercise the
|
||||
* paths that nullable-pointer unions (nullable-unions.ts) bypass. */
|
||||
* paths that nullable-pointer unions (nullable-unions.ts) bypass. A union of
|
||||
* records alone is a Set element by record identity, so for those the Set
|
||||
* holds a companion union with a number arm, which keeps the records out of
|
||||
* every shape union. */
|
||||
export function keepUnionsBoxed(mod: IrModule): IrModule {
|
||||
const keep = (mod.unions ?? []).map((union) => ({
|
||||
id: `%g.keep.${union.id}`,
|
||||
name: `keep_${union.id}`,
|
||||
type: { kind: "set", elem: { kind: "union", unionId: union.id } } as IrType,
|
||||
mutable: false,
|
||||
}));
|
||||
return { ...mod, globals: [...(mod.globals ?? []), ...keep] };
|
||||
const companions: IrUnionDef[] = [];
|
||||
const keep = (mod.unions ?? []).map((union) => {
|
||||
let elem = union.id;
|
||||
if (union.arms.every((arm) => arm.kind === "record")) {
|
||||
elem = `%keep.${union.id}`;
|
||||
companions.push({ id: elem, arms: [...union.arms, F64] });
|
||||
}
|
||||
return {
|
||||
id: `%g.keep.${union.id}`,
|
||||
name: `keep_${union.id}`,
|
||||
type: { kind: "set", elem: { kind: "union", unionId: elem } } as IrType,
|
||||
mutable: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...mod,
|
||||
unions: [...(mod.unions ?? []), ...companions],
|
||||
globals: [...(mod.globals ?? []), ...keep],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { UnwindAbiViolation, bridgePlainUnwindCalls } from "./unwind-abi.js";
|
||||
|
||||
async function moduleFor(source: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unwind-abi-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The definition of the source function `name` (its i32 or borrowing entry when it has one). */
|
||||
function body(ir: string, name: string): string {
|
||||
return new RegExp(
|
||||
`^define internal [^\\n]*@sc_b?f_[^\\n(]*${name}(?:\\.i32)?\\([^]*?^}`,
|
||||
"m",
|
||||
).exec(ir)![0];
|
||||
}
|
||||
|
||||
const SOURCE = `
|
||||
function leaf(n: number): number {
|
||||
if (n === 3) throw new RangeError("three");
|
||||
return n * 2;
|
||||
}
|
||||
function first(n: number): number {
|
||||
return leaf(n) + 1;
|
||||
}
|
||||
function second(n: number): number {
|
||||
return first(n) * 3;
|
||||
}
|
||||
function third(n: number): number {
|
||||
return second(n) - 2;
|
||||
}
|
||||
function mid(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = third(n);
|
||||
parts.push("b");
|
||||
return r + parts.length;
|
||||
}
|
||||
class Counter {
|
||||
total = 0;
|
||||
add(n: number): void { this.total += mid(n); }
|
||||
}
|
||||
function outer(n: number): string {
|
||||
try { return "ok " + mid(n); } catch (e) { return "caught " + (e as Error).message; }
|
||||
}
|
||||
const counter = new Counter();
|
||||
for (let i = 0; i < 5; i++) { console.log(outer(i)); try { counter.add(i); } catch { console.log("add", i); } }
|
||||
const f = (x: number) => mid(x);
|
||||
console.log(f(1), counter.total);
|
||||
`;
|
||||
|
||||
const LINUX = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const;
|
||||
|
||||
test("directly called functions raise, and their callers invoke them without a pending check", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// leaf throws: the exception starts on the pending flag, and the three
|
||||
// calls above it test the flag too; mid raises it on.
|
||||
for (const name of ["leaf", "first", "second", "third"])
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
const mid = body(ir, "mid");
|
||||
expect(mid).toMatch(/= call double @sc_b?f_[^(]*third\(/);
|
||||
expect(mid).toContain("call void @scr_unwind_raise()");
|
||||
// outer invokes mid; the landing pad takes the exception to its handler.
|
||||
const outer = body(ir, "outer");
|
||||
expect(outer).toMatch(
|
||||
/= invoke double @sc_b?f_[^(]*mid[^\n]* to label %inv\.ok\d+ unwind label %inv\.lp\d+/,
|
||||
);
|
||||
expect(outer).toMatch(/inv\.lp\d+:\n\s+%t\d+ = landingpad \{ ptr, i32 \} cleanup/);
|
||||
expect(outer).toContain("personality ptr @__gcc_personality_v0");
|
||||
expect(ir).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
expect(ir).toContain("declare void @scr_unwind_raise() cold");
|
||||
});
|
||||
|
||||
test("a method of a class outside any hierarchy is on the unwinding ABI; closures keep the flag", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// Counter.add invokes mid and resumes the unwind from its landing pad; the
|
||||
// closure over mid is a pointer target and returns with the flag.
|
||||
const add = body(ir, "Counter_add");
|
||||
expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/);
|
||||
expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/);
|
||||
expect(add).not.toContain("@scr_unwind_raise");
|
||||
const closures = ir
|
||||
.split(/\n(?=define )/)
|
||||
.filter((def) => /^define internal [^\n]*@sc_b?f_[^\n(]*fn_/.test(def));
|
||||
expect(closures.length).toBeGreaterThan(0);
|
||||
for (const def of closures) expect(def).not.toContain("call void @scr_unwind_raise()");
|
||||
});
|
||||
|
||||
test("recursive functions keep the pending flag, also when they recurse through a callback", async () => {
|
||||
const source = `
|
||||
function leaf(n: number): number {
|
||||
if (n < 0) throw new RangeError("negative");
|
||||
return n;
|
||||
}
|
||||
function one(n: number): number { return leaf(n) + 1; }
|
||||
function two(n: number): number { return one(n) + 1; }
|
||||
function three(n: number): number { return two(n) + 1; }
|
||||
function four(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = three(n);
|
||||
return r + parts.length;
|
||||
}
|
||||
function walk(n: number, depth: number): number {
|
||||
const parts: string[] = ["w" + n];
|
||||
const r = depth === 0 ? four(n) : walk(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const parts: string[] = ["v" + n];
|
||||
const r = depth === 0 ? four(n) : each(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (let i = -1; i < 2; i++) {
|
||||
try { console.log(walk(i, 3), visit(i, 3, each)); } catch (e) { console.log((e as Error).message); }
|
||||
}
|
||||
`;
|
||||
const ir = emitLlvmModule(await moduleFor(source), LINUX);
|
||||
expect(body(ir, "four")).toContain("call void @scr_unwind_raise()");
|
||||
for (const name of ["walk", "visit"]) {
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
expect(body(ir, name)).toMatch(/invoke double @sc_b?f_[^(]*four/);
|
||||
}
|
||||
});
|
||||
|
||||
test("without the option, and off Linux, emission keeps the pending-flag ABI", async () => {
|
||||
const mod = await moduleFor(SOURCE);
|
||||
for (const options of [{}, { targetTriple: "arm64-apple-macosx14.0.0", unwindAbi: true }]) {
|
||||
const ir = emitLlvmModule(mod, options);
|
||||
expect(ir).not.toContain("invoke ");
|
||||
expect(ir).not.toContain("@scr_unwind_raise");
|
||||
expect(ir).not.toContain("personality");
|
||||
}
|
||||
});
|
||||
|
||||
test("bridgePlainUnwindCalls keeps the pending-flag contract at plain calls of raising functions", () => {
|
||||
const raising = [
|
||||
"define internal ptr @sc_bf_raises(ptr %p) #0 { ; raises",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal ptr @sc_f_adapter(ptr %p0) #0 {",
|
||||
"entry:",
|
||||
" %result = call ptr @sc_bf_raises(ptr %p0) ; borrowed result",
|
||||
" br label %next",
|
||||
"next:",
|
||||
" %v = phi ptr [ %result, %entry ]",
|
||||
" ret ptr %v",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([`${raising}\n${caller}`]);
|
||||
expect(out).toContain(
|
||||
"%result.ub = invoke ptr @sc_bf_raises(ptr %p0) to label %ub.n.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("%result = phi ptr [ %result.ub, %ub.n.0 ], [ null, %ub.lp.0 ]");
|
||||
// The split block's successor now comes from its last part.
|
||||
expect(out).toContain("%v = phi ptr [ %result, %ub.ok.0 ]");
|
||||
expect(out).toMatch(
|
||||
/define internal ptr @sc_f_adapter\(ptr %p0\) #0 personality ptr @__gcc_personality_v0 \{/,
|
||||
);
|
||||
expect(out).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
});
|
||||
|
||||
test("an invoked wrapper of a raising function resumes the unwind; its address must not escape", () => {
|
||||
const raising = [
|
||||
"define internal double @sc_bf_body.i32(i32 %a) #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const wrapper = [
|
||||
"define internal double @sc_bf_body(double %a) #0 {",
|
||||
"entry:",
|
||||
" %i = fptosi double %a to i32",
|
||||
" %r = call double @sc_bf_body.i32(i32 %i)",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal double @sc_bf_caller(double %a) #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
" %r = invoke double @sc_bf_body(double %a) to label %ok unwind label %lp",
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([[raising, wrapper, caller].join("\n")]);
|
||||
expect(out).toContain(
|
||||
"%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("resume { ptr, i32 } %ub.v.0");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
try {
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]);
|
||||
} catch (error) {
|
||||
expect([...(error as UnwindAbiViolation).symbols]).toEqual(["sc_bf_body.i32"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("an invoked function that never tests the pending flag needs no raise", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises() #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const leaf = [
|
||||
"define internal double @sc_bf_leaf() #0 {",
|
||||
"entry:",
|
||||
" ret double 1.0",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = (callee: string) =>
|
||||
[
|
||||
"define internal double @sc_bf_caller() #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
` %r = invoke double @${callee}() to label %ok unwind label %lp`,
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, leaf, caller("sc_bf_leaf")].join("\n")]),
|
||||
).not.toThrow();
|
||||
const testing = leaf.replace(
|
||||
" ret double 1.0",
|
||||
" %f = load i32, ptr @scr_exc_flag\n ret double 1.0",
|
||||
);
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, testing, caller("sc_bf_leaf")].join("\n")]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
});
|
||||
|
||||
test("calls that cannot unwind are marked nounwind", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises(ptr %b) #0 {",
|
||||
"entry:",
|
||||
" %n = call i64 @scr_bytes_length(ptr %b) ; length",
|
||||
" call void @scr_bytes_release(ptr %b), !dbg !4",
|
||||
" %r = call ptr %b(ptr %b)",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([raising]);
|
||||
expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length");
|
||||
expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4");
|
||||
expect(out).toContain("%r = call ptr %b(ptr %b) nounwind");
|
||||
expect(out).toContain(" call void @scr_unwind_raise()\n");
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
/* The functions that propagate exceptions by unwinding instead of the
|
||||
* pending flag (see unwindAbiFunctions). */
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** How many calls above a `throw` functions keep the pending flag. */
|
||||
const THROW_LEVELS = 3;
|
||||
/** The call graph's node for calls through values. */
|
||||
const ANY_FUNCTION = " any function";
|
||||
|
||||
/** The nodes of `edges` on a cycle (a recursion), by Tarjan's algorithm
|
||||
* without recursion of its own: a large module's call chains are deeper
|
||||
* than the host stack. */
|
||||
function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<string> {
|
||||
const index = new Map<string, number>();
|
||||
const low = new Map<string, number>();
|
||||
const stack: string[] = [];
|
||||
const onStack = new Set<string>();
|
||||
const result = new Set<string>();
|
||||
let counter = 0;
|
||||
const enter = (name: string): { name: string; next: number } => {
|
||||
index.set(name, counter);
|
||||
low.set(name, counter++);
|
||||
stack.push(name);
|
||||
onStack.add(name);
|
||||
return { name, next: 0 };
|
||||
};
|
||||
for (const root of edges.keys()) {
|
||||
if (index.has(root)) continue;
|
||||
const work = [enter(root)];
|
||||
while (work.length > 0) {
|
||||
const top = work[work.length - 1]!;
|
||||
const out = edges.get(top.name) ?? [];
|
||||
if (top.next < out.length) {
|
||||
const callee = out[top.next++]!;
|
||||
if (!index.has(callee)) work.push(enter(callee));
|
||||
else if (onStack.has(callee))
|
||||
low.set(top.name, Math.min(low.get(top.name)!, index.get(callee)!));
|
||||
continue;
|
||||
}
|
||||
work.pop();
|
||||
const parent = work[work.length - 1];
|
||||
if (parent !== undefined)
|
||||
low.set(parent.name, Math.min(low.get(parent.name)!, low.get(top.name)!));
|
||||
if (low.get(top.name) !== index.get(top.name)) continue;
|
||||
const members: string[] = [];
|
||||
let member: string;
|
||||
do {
|
||||
member = stack.pop()!;
|
||||
onStack.delete(member);
|
||||
members.push(member);
|
||||
} while (member !== top.name);
|
||||
if (members.length > 1 || out.includes(top.name)) for (const m of members) result.add(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The may-throw functions whose exceptions reach their callers by
|
||||
* unwinding the native stack (scr_unwind_raise) instead of returning with
|
||||
* the pending flag set. Their callers call them with `invoke`, so the call
|
||||
* needs no flag test; the landing pad runs the frame's unwind.
|
||||
*
|
||||
* A function qualifies only when every way it can be reached is a direct
|
||||
* `call` from emitted code that can lower it as an `invoke`: it is not
|
||||
* async or a generator, not a constructor, not a method a virtual call can
|
||||
* reach or one a vtable holds (an overridden method), not a closure
|
||||
* target, not the entry or a library export, not named by any node other
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a
|
||||
* few nanoseconds for a flag test, so the functions an exception is likely to
|
||||
* cross many of keep the flag: those with a `throw` or rethrow of their own
|
||||
* and those up to THROW_LEVELS calls above one (a handler near the throw
|
||||
* takes it without a native unwind), and the recursive ones (deep stacks
|
||||
* are recursions). */
|
||||
export function unwindAbiFunctions(
|
||||
mod: IrModule,
|
||||
mayThrow: ReadonlySet<string>,
|
||||
excluded: ReadonlySet<string> = new Set(),
|
||||
): Set<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const blocked = new Set<string>(excluded);
|
||||
blocked.add(mod.entry);
|
||||
// The runtime calls the error property readers through the pointers the
|
||||
// module installs (emit-module.ts).
|
||||
blocked.add("%error.name.read");
|
||||
blocked.add("%error.message.read");
|
||||
for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName);
|
||||
// Methods a virtual call can reach, and the overridden methods of class
|
||||
// hierarchies (whose vtables list them), are called through pointers; the
|
||||
// others are called directly, like functions. A virtual call reaches its listed
|
||||
// targets, or else the method in its receiver class's hierarchy; a
|
||||
// receiver that is no class of the module (an interface) may reach any
|
||||
// class's method of that name.
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const anyClass = new Set<string>();
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
if (e.kind !== "virtualCall") return true;
|
||||
if (e.targets !== undefined) for (const target of e.targets) blocked.add(target);
|
||||
else if (classes.has(e.className)) blocked.add(`%${e.className}.${e.method}`);
|
||||
else anyClass.add(e.method);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
// A hierarchy's vtables hold the methods declared by more than one of its
|
||||
// classes (an override and what it overrides).
|
||||
const rootOf = (name: string): string => {
|
||||
let cls = classes.get(name);
|
||||
while (cls?.base !== undefined && classes.has(cls.base)) cls = classes.get(cls.base);
|
||||
return cls?.name ?? name;
|
||||
};
|
||||
const declarers = new Map<string, number>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
const key = `${rootOf(cls.name)}\0${m}`;
|
||||
declarers.set(key, (declarers.get(key) ?? 0) + 1);
|
||||
}
|
||||
for (const cls of mod.classes ?? []) {
|
||||
blocked.add(`%${cls.name}.constructor`);
|
||||
blocked.add(`%${cls.name}.construct`);
|
||||
const root = rootOf(cls.name);
|
||||
for (const m of cls.methods ?? [])
|
||||
if (anyClass.has(m) || declarers.get(`${root}\0${m}`)! > 1) blocked.add(`%${cls.name}.${m}`);
|
||||
}
|
||||
// Any function a node names other than as a call's callee may be reached
|
||||
// through a pointer.
|
||||
const referenced = new Set<string>();
|
||||
const noteNames = (node: object, skip: string | undefined): void => {
|
||||
for (const key in node) {
|
||||
if (key === skip) continue;
|
||||
const value = (node as Record<string, unknown>)[key];
|
||||
if (typeof value === "string" && names.has(value)) referenced.add(value);
|
||||
}
|
||||
};
|
||||
// The call graph, for the functions near a throw and the recursive ones.
|
||||
// Calls through values (closures, class values, dynamic calls, callbacks
|
||||
// passed to the runtime) reach ANY_FUNCTION, which reaches every function
|
||||
// a value can hold.
|
||||
const throws = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const callees = new Map<string, string[]>();
|
||||
const edge = (from: string, to: string): void => {
|
||||
let list = callers.get(to);
|
||||
if (list === undefined) callers.set(to, (list = []));
|
||||
list.push(from);
|
||||
let out = callees.get(from);
|
||||
if (out === undefined) callees.set(from, (out = []));
|
||||
out.push(to);
|
||||
};
|
||||
const methodsNamed = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodsNamed.get(m);
|
||||
if (list === undefined) methodsNamed.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
for (const fn of mod.functions) {
|
||||
const coroutine = fn.async === true || fn.generator !== undefined;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
const kind = e.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
if (e.kind === "call") {
|
||||
edge(fn.name, e.callee);
|
||||
// The entry's callees (the module initializers) run once: on the
|
||||
// flag they keep landing pads out of the top-level code inlined
|
||||
// into the entry.
|
||||
if (coroutine || fn.name === mod.entry) blocked.add(e.callee);
|
||||
noteNames(e, "callee");
|
||||
return true;
|
||||
}
|
||||
noteNames(e, undefined);
|
||||
if (e.kind === "virtualCall")
|
||||
for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target);
|
||||
else if (e.kind === "new") {
|
||||
edge(fn.name, `%${e.className}.constructor`);
|
||||
edge(fn.name, `%${e.className}.construct`);
|
||||
} else if (kind === "callValue" || kind === "dynCall" || kind === "newValue")
|
||||
edge(fn.name, ANY_FUNCTION);
|
||||
else if ("args" in e) {
|
||||
const args: unknown = e.args;
|
||||
if (Array.isArray(args))
|
||||
for (const arg of args as IrExpr[])
|
||||
if (arg.type.kind === "func") edge(fn.name, ANY_FUNCTION);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
const kind = s.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
noteNames(s, undefined);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const name of referenced) {
|
||||
blocked.add(name);
|
||||
edge(ANY_FUNCTION, name);
|
||||
}
|
||||
// A throw starts on the pending flag, and so do the functions up to
|
||||
// THROW_LEVELS calls above it. The union narrowing helpers' throws (a
|
||||
// failed narrowing) don't count for their callers.
|
||||
for (const name of throws) blocked.add(name);
|
||||
let level = [...throws].filter((name) => !name.startsWith("%union."));
|
||||
const near = new Set(level);
|
||||
for (let i = 0; i < THROW_LEVELS; i++) {
|
||||
const next: string[] = [];
|
||||
for (const name of level)
|
||||
for (const caller of callers.get(name) ?? [])
|
||||
if (!near.has(caller)) {
|
||||
near.add(caller);
|
||||
next.push(caller);
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
for (const name of near) blocked.add(name);
|
||||
for (const name of recursiveFunctions(callees)) blocked.add(name);
|
||||
const result = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
if (!mayThrow.has(fn.name) || blocked.has(fn.name)) continue;
|
||||
if (fn.async === true || fn.generator !== undefined) continue;
|
||||
result.add(fn.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** What an unwinding-ABI function's unwind ends with when no handler of
|
||||
* its own takes the exception. */
|
||||
export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable";
|
||||
/** What the unwind of a landing pad ends with when no handler of the
|
||||
* function takes the exception: the unwind goes on (`resume`) with the
|
||||
* exception the landing pad stored in UNWIND_SLOT. The block builder
|
||||
* expands it (BlockBuilder.terminate). */
|
||||
export const UNWIND_RESUME = "resume { ptr, i32 } <unwind slot>";
|
||||
/** The function's slot for the exception its landing pads take. */
|
||||
export const UNWIND_SLOT = "%unw.exc";
|
||||
/** scr_unwind_raise's declaration. It is not `noreturn`: sanitized builds
|
||||
* would call the sanitizer's no-return hook before it, which fails on fiber
|
||||
* stacks; scr_unwind_raise clears the unwound frames' shadow itself. */
|
||||
export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold";
|
||||
|
||||
/** The personality of functions with landing pads: the landing pads are
|
||||
* cleanups that scr_unwind_raise's forced unwind runs. */
|
||||
export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0";
|
||||
|
||||
/** An unwind leaving the function: a raise, or a landing pad's `resume`. */
|
||||
const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /;
|
||||
/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */
|
||||
const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/;
|
||||
|
||||
/** Functions emission found breaking the unwinding ABI's rules
|
||||
* (bridgePlainUnwindCalls), by symbol: the caller emits the module again
|
||||
* with them on the pending-flag ABI. */
|
||||
export class UnwindAbiViolation extends Error {
|
||||
readonly symbols: ReadonlySet<string>;
|
||||
constructor(symbols: ReadonlySet<string>) {
|
||||
super(`unwinding ABI: ${[...symbols].slice(0, 3).join(", ")} must keep the pending flag`);
|
||||
this.symbols = symbols;
|
||||
}
|
||||
}
|
||||
const DEFINE_NAME = /^define [^@\n]*@([\w.$%-]+)\(/;
|
||||
const PLAIN_CALL = /^(\s*)(%[\w.$-]+) = call (.*?)@([\w.$%-]+)\(|^(\s*)call (.*?)@([\w.$%-]+)\(/;
|
||||
const RETURN_ATTRIBUTES =
|
||||
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
|
||||
|
||||
/** A call instruction's line. */
|
||||
const CALL_LINE = /^\s*(?:%[\w.$-]+ = )?(?:(?:tail|musttail|notail) )?call /;
|
||||
/** A call's callee (a function or a register) and the parenthesis that
|
||||
* opens its arguments. */
|
||||
const CALLEE = /[@%][\w.$%-]+\(/;
|
||||
|
||||
/** The index of the parenthesis that closes the argument list opened at `open`. */
|
||||
function argumentsEnd(line: string, open: number): number {
|
||||
let depth = 0;
|
||||
let close = open;
|
||||
for (; close < line.length; close++) {
|
||||
const c = line.charAt(close);
|
||||
if (c === "(") depth++;
|
||||
else if (c === ")" && --depth === 0) break;
|
||||
}
|
||||
return close;
|
||||
}
|
||||
|
||||
/** A call that cannot unwind, marked so: only scr_unwind_raise and the
|
||||
* raising functions propagate an unwind (runtime code and every function
|
||||
* reached through a pointer return normally), so every other call is
|
||||
* `nounwind`, and the inliner keeps it a call where it inlines a raising
|
||||
* function at an invoke. */
|
||||
function markNounwind(line: string): string {
|
||||
const call = line.indexOf("call ");
|
||||
const callee = CALLEE.exec(line.slice(call));
|
||||
if (callee === null || callee[0] === "@scr_unwind_raise(") return line;
|
||||
const close = argumentsEnd(line, call + callee.index + callee[0].length - 1);
|
||||
return close >= line.length
|
||||
? line
|
||||
: `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`;
|
||||
}
|
||||
|
||||
/** The value a bridged call yields on its landing pad: the dummy a
|
||||
* pending-flag callee returns with an exception pending. */
|
||||
function dummyValue(type: string): string {
|
||||
if (type === "ptr") return "null";
|
||||
if (type === "double" || type === "float") return "0.0";
|
||||
if (/^i\d+$/.test(type)) return "0";
|
||||
return "zeroinitializer";
|
||||
}
|
||||
|
||||
/** Rewrites every remaining plain call of an unwinding-ABI function (one
|
||||
* that raises, scr_unwind_raise, or resumes an unwind) in `parts`, the module's text, into an
|
||||
* invoke whose landing pad continues after the call with the exception
|
||||
* pending and the call's dummy result: the pending-flag contract those
|
||||
* call sites were emitted for, whichever lowering emitted them. Calls of
|
||||
* these functions that emitDirectCall lowered are invokes already. */
|
||||
export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
const raisers = new Set<string>();
|
||||
const split = parts.map((part) => part.split(/\n(?=define )/));
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && UNWINDS.test(chunk)) raisers.add(m[1]!);
|
||||
}
|
||||
if (raisers.size === 0) return parts;
|
||||
// An invoke's callee must raise or never test the pending flag: one that
|
||||
// returned with the flag pending would pass the exception by its invoking
|
||||
// caller unseen. A wrapper an invoke reaches (an int32 or borrowed-ABI
|
||||
// entry forwarding to the body) propagates its callee's unwind instead
|
||||
// (`resume`), so it raises too. A raising function must be reached only by
|
||||
// calls: a pointer to it (a vtable or table entry, a runtime callback)
|
||||
// could be called without a landing pad. Functions that break either rule
|
||||
// go back to the pending-flag ABI (UnwindAbiViolation).
|
||||
const violations = new Set<string>();
|
||||
const invoked = new Set<string>();
|
||||
const testsFlag = new Set<string>();
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && PENDING_TEST.test(chunk)) testsFlag.add(m[1]!);
|
||||
for (const c of chunk.matchAll(/ invoke [^@\n]*@([\w.$%-]+)\(/g)) invoked.add(c[1]!);
|
||||
}
|
||||
const resumers = new Set<string>();
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
const name = m?.[1];
|
||||
if (name === undefined || raisers.has(name) || !invoked.has(name)) continue;
|
||||
if (![...chunk.matchAll(/ call [^@\n]*@([\w.$%-]+)\(/g)].some((c) => raisers.has(c[1]!)))
|
||||
continue;
|
||||
if (testsFlag.has(name)) continue;
|
||||
raisers.add(name);
|
||||
resumers.add(name);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name);
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks)
|
||||
for (const line of chunk.split("\n")) {
|
||||
if (line.startsWith("define ")) continue;
|
||||
for (const m of line.matchAll(/@([\w.$%-]+)/g)) {
|
||||
if (!raisers.has(m[1]!)) continue;
|
||||
const at = m.index!;
|
||||
if (
|
||||
/(?:call|invoke) [^@]*$/.test(line.slice(0, at)) &&
|
||||
line.charAt(at + m[0].length) === "("
|
||||
)
|
||||
continue;
|
||||
violations.add(m[1]!);
|
||||
}
|
||||
}
|
||||
if (violations.size > 0) throw new UnwindAbiViolation(violations);
|
||||
let personality = false;
|
||||
let counter = 0;
|
||||
const out = split.map((chunks) =>
|
||||
chunks
|
||||
.map((chunk) => {
|
||||
if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk;
|
||||
const lines = chunk.split("\n");
|
||||
const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!);
|
||||
const result: string[] = [];
|
||||
// The label a block's terminator now follows, for phi operands.
|
||||
const moved = new Map<string, string>();
|
||||
let block = "";
|
||||
let changed = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
const label = /^([\w.$-]+):/.exec(line);
|
||||
if (label !== null) block = label[1]!;
|
||||
const m = PLAIN_CALL.exec(line);
|
||||
const callee = m === null ? undefined : (m[4] ?? m[7]);
|
||||
if (m === null || callee === undefined || !raisers.has(callee)) {
|
||||
const marked = CALL_LINE.test(line) ? markNounwind(line) : line;
|
||||
changed ||= marked !== line;
|
||||
result.push(marked);
|
||||
continue;
|
||||
}
|
||||
// The call's text from its callee to the end of the argument list.
|
||||
const close = argumentsEnd(line, line.indexOf(`@${callee}(`) + callee.length + 1);
|
||||
const head = line.slice(line.indexOf("call ") + 5, close + 1);
|
||||
const tail = line.slice(close + 1);
|
||||
const meta = /,\s*!/.exec(tail);
|
||||
const attrs = (
|
||||
meta === null ? tail.replace(/;.*$/, "") : tail.slice(0, meta.index)
|
||||
).trimEnd();
|
||||
const dbg = meta === null ? "" : tail.slice(meta.index).replace(/\s;.*$/, "");
|
||||
const n = counter++;
|
||||
if (block === "") {
|
||||
result.splice(1, 0, "entry.ub:");
|
||||
block = "entry.ub";
|
||||
}
|
||||
const indent = m[1] ?? m[5] ?? " ";
|
||||
const ok = `ub.ok.${n}`;
|
||||
const pad = `ub.lp.${n}`;
|
||||
if (resume) {
|
||||
// The wrapper holds nothing to release: the unwind goes on to
|
||||
// its caller's landing pad.
|
||||
const assign = m[2] !== undefined ? `${m[2]} = ` : "";
|
||||
result.push(
|
||||
`${indent}${assign}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}resume { ptr, i32 } %ub.v.${n}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
} else if (m[2] !== undefined) {
|
||||
const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim();
|
||||
const normal = `ub.n.${n}`;
|
||||
result.push(
|
||||
`${indent}${m[2]}.ub = invoke ${head}${attrs} to label %${normal} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(`${normal}:`, `${indent}br label %${ok}`);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(
|
||||
`${ok}:`,
|
||||
`${indent}${m[2]} = phi ${type} [ ${m[2]}.ub, %${normal} ], [ ${dummyValue(type)}, %${pad} ]`,
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
`${indent}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
}
|
||||
moved.set(block, ok);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return chunk;
|
||||
if (moved.size === 0) return result.join("\n");
|
||||
personality = true;
|
||||
const header = result[0]!;
|
||||
const brace = header.lastIndexOf(" {");
|
||||
if (!header.includes("personality") && brace >= 0) {
|
||||
// Before the function's metadata attachments (`!dbg`), if any.
|
||||
const dbg = header.slice(0, brace).search(/\s!dbg !\d+$/);
|
||||
const at = dbg >= 0 ? dbg : brace;
|
||||
result[0] = `${header.slice(0, at)} ${UNWIND_PERSONALITY}${header.slice(at)}`;
|
||||
}
|
||||
// A split block's successors now come from its last part.
|
||||
if (moved.size > 0)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const line = result[i]!;
|
||||
if (!line.includes(" phi ")) continue;
|
||||
result[i] = line.replace(/, %([\w.$-]+) \]/g, (all, name: string) => {
|
||||
const to = moved.get(name);
|
||||
return to === undefined ? all : `, %${to} ]`;
|
||||
});
|
||||
}
|
||||
return result.join("\n");
|
||||
})
|
||||
.join("\n"),
|
||||
);
|
||||
if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)")))
|
||||
out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`;
|
||||
return out;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user