mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge main
This commit is contained in:
@@ -547,6 +547,8 @@ jobs:
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (tsgo and tsc-ts give the same IR)
|
||||
runs-on: ubuntu-24.04
|
||||
# The two corpus compiles run back to back and take close to 30 minutes on a
|
||||
# slow hosted runner, so a 30-minute limit cancels otherwise healthy runs.
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -641,8 +643,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:
|
||||
@@ -559,6 +630,21 @@ jobs:
|
||||
- name: Package standalone compilers
|
||||
run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets
|
||||
|
||||
# The profile the native CLIs were built with. Runs that reuse native
|
||||
# builds from before the cli-profile job, or whose profile job failed,
|
||||
# have none; their CLIs used the committed profile.
|
||||
- name: Download the native compiler profile
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUN: ${{ needs.check-release.outputs.artifact_run }}
|
||||
VERSION: ${{ needs.check-release.outputs.version }}
|
||||
run: |
|
||||
if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then
|
||||
mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz"
|
||||
else
|
||||
echo "Run $RUN has no cli-profile artifact"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
|
||||
+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 |
|
||||
@@ -30,6 +31,7 @@ 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 |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/date-math.ts",
|
||||
"args": [
|
||||
"40"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
|
||||
// UTC and local setters, weekday and month-end rules read back through the
|
||||
// getters, Dates compared and sorted by time value, and ISO text for a
|
||||
// sample. Argument: the number of rounds.
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "40");
|
||||
let seed = 2026;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
/** The next business day at 09:00 local time on or after `from`. */
|
||||
function nextBusinessDay(from: Date): Date {
|
||||
const day = new Date(from.getTime());
|
||||
day.setHours(9, 0, 0, 0);
|
||||
if (day < from) day.setDate(day.getDate() + 1);
|
||||
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
|
||||
return day;
|
||||
}
|
||||
|
||||
/** The last day of `date`'s month, in UTC. */
|
||||
function monthEnd(date: Date): number {
|
||||
const end = new Date(date.getTime());
|
||||
end.setUTCDate(1);
|
||||
end.setUTCMonth(end.getUTCMonth() + 1);
|
||||
end.setUTCDate(0);
|
||||
return end.getUTCDate();
|
||||
}
|
||||
|
||||
let checksum = 0;
|
||||
let sample = "";
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const due: Date[] = [];
|
||||
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
|
||||
const next = nextBusinessDay(cursor);
|
||||
checksum += next.getDate() + monthEnd(next) + next.getHours();
|
||||
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
|
||||
due.push(next);
|
||||
}
|
||||
due.sort((a, b) => +a - +b);
|
||||
let previous = due[0]!;
|
||||
for (const date of due) {
|
||||
checksum += (+date - +previous) / 3600000;
|
||||
previous = date;
|
||||
}
|
||||
sample = due[due.length >> 1]!.toISOString();
|
||||
}
|
||||
console.log(Math.round(checksum), sample);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/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)));
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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: 42, marker: "scr_runtime_abi_v42" },
|
||||
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_v42");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -295,13 +295,16 @@ static void stop_process(Ts7Process *p) {
|
||||
close(p->socket);
|
||||
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);
|
||||
@@ -405,3 +408,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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
{ "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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,5 +21,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
|
||||
|
||||
@@ -20,6 +20,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
installDarwinDebugSymbols,
|
||||
@@ -240,8 +241,11 @@ export async function linkNativeExecutable(
|
||||
try {
|
||||
const staged = await stageRuntimePackArtifacts(plan.runtimePack);
|
||||
stagedRoot = staged.root;
|
||||
// Linux links with lld when the default driver finds it.
|
||||
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
|
||||
const args = [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...plan.systemLibraries.map((name) => `-l${name}`),
|
||||
@@ -276,7 +280,15 @@ export async function linkNativeExecutable(
|
||||
...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])),
|
||||
])
|
||||
.catch(() => null);
|
||||
await execFileAsync(linker, args);
|
||||
await execFileAsync(linker, args).catch(async (error: unknown) => {
|
||||
// A failed link with lld runs again with the driver's default linker,
|
||||
// whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
await execFileAsync(
|
||||
linker,
|
||||
args.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
});
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
if (plan.darwinDebugSymbols) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function tool(path: string): string {
|
||||
writeFileSync(path, "#!/bin/sh\n");
|
||||
chmodSync(path, 0o755);
|
||||
return path;
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "scriptc-lld-"));
|
||||
directories.push(root);
|
||||
const llvm = join(root, "llvm", "bin");
|
||||
const usr = join(root, "usr", "bin");
|
||||
const other = join(root, "other");
|
||||
for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true });
|
||||
return { root, llvm, usr, other };
|
||||
}
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a driver finds ld.lld beside itself, beside its real executable, or on PATH",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
// Without ld.lld anywhere.
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(false);
|
||||
// Beside the driver.
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
expect(driverFindsLld(clang, { PATH: f.other })).toBe(true);
|
||||
// A PATH entry that links to the driver: clang looks beside its real executable.
|
||||
symlinkSync(clang, join(f.usr, "clang"));
|
||||
expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true);
|
||||
// A driver elsewhere finds ld.lld on PATH, and not otherwise.
|
||||
const gcc = tool(join(f.other, "gcc"));
|
||||
expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false);
|
||||
expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true);
|
||||
expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"Linux links with the default driver use lld when the driver finds it",
|
||||
() => {
|
||||
const f = fixture();
|
||||
const clang = tool(join(f.llvm, "clang"));
|
||||
tool(join(f.llvm, "ld.lld"));
|
||||
const env = { PATH: f.other };
|
||||
expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]);
|
||||
expect(lldLinkerArgs("darwin", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("windows", clang, env)).toEqual([]);
|
||||
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The executable `command` names: a path, or the first match on PATH. */
|
||||
function commandFile(command: string, env: NodeJS.ProcessEnv): string | null {
|
||||
if (command.includes("/")) return isFile(command) ? resolve(command) : null;
|
||||
for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
|
||||
const candidate = join(directory || ".", command);
|
||||
if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in
|
||||
* the directory of the driver's executable (where clang looks first) or on
|
||||
* PATH. Linking still falls back to the driver's default linker when a link
|
||||
* with lld fails.
|
||||
*/
|
||||
export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const file = commandFile(driver, env);
|
||||
if (file === null) return false;
|
||||
try {
|
||||
for (const directory of new Set([dirname(file), dirname(realpathSync(file))]))
|
||||
if (isFile(join(directory, "ld.lld"))) return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return commandFile("ld.lld", env) !== null;
|
||||
}
|
||||
|
||||
/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */
|
||||
export function lldLinkerArgs(
|
||||
platform: string,
|
||||
driver: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env)
|
||||
? ["-fuse-ld=lld"]
|
||||
: [];
|
||||
}
|
||||
@@ -9,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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
stringScanType,
|
||||
} from "./string-reads.js";
|
||||
import { unitSafeLoop } from "./string-unit-versions.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -73,6 +79,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. */
|
||||
@@ -114,6 +128,9 @@ 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;
|
||||
@@ -670,6 +687,19 @@ export function matchBytesLoopVersion(
|
||||
// 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 +709,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,
|
||||
@@ -1018,13 +1051,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 +1085,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();
|
||||
@@ -1158,7 +1217,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();
|
||||
@@ -1215,8 +1274,70 @@ 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, after one runtime read publishes it
|
||||
* as the scan receiver, the published unit count (0 when the runtime does
|
||||
* not hold its 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);
|
||||
host.declare(STRING_CHAR_CODE_AT_DECL);
|
||||
const read = B.tmp(),
|
||||
receiver = B.tmp(),
|
||||
same = B.tmp(),
|
||||
pointerSlot = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`);
|
||||
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 {
|
||||
|
||||
@@ -50,8 +50,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 +65,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));
|
||||
|
||||
@@ -122,6 +122,7 @@ const counted = (
|
||||
guarded: false,
|
||||
limitGuarded: false,
|
||||
startGuarded: false,
|
||||
startBound: Number.MAX_SAFE_INTEGER,
|
||||
guardLimit: 0,
|
||||
followers: [],
|
||||
...fields,
|
||||
|
||||
@@ -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
|
||||
@@ -1825,11 +1827,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} {`,
|
||||
@@ -2917,14 +2926,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 +2948,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 +2964,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 +3148,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;
|
||||
|
||||
@@ -56,11 +56,14 @@ 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.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
@@ -136,6 +139,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
@@ -596,6 +600,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
B.terminate(cleanup.terminator);
|
||||
}
|
||||
|
||||
if (emitter.usesLandingPads && emitter.currentUnwindAbi)
|
||||
B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`);
|
||||
if (emitter.logArgSlots > 0) {
|
||||
B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`);
|
||||
}
|
||||
@@ -636,7 +642,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);
|
||||
|
||||
@@ -63,7 +63,7 @@ import { nodeIslandManifest } from "../../node-islands/modules.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { NULLABLE_ABSENT, NULLABLE_NULL } from "./nullable-unions.js";
|
||||
import { literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
@@ -1667,9 +1667,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;
|
||||
|
||||
@@ -994,7 +994,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
LOOPS.of(emitter).denseLoop = dense;
|
||||
@@ -1018,7 +1018,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
LOOPS.of(emitter).byteWindowsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.integerRanges = window.ranges;
|
||||
@@ -1072,7 +1072,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
B.br(countedJoin);
|
||||
B.startBlock(fast);
|
||||
@@ -2049,7 +2049,7 @@ function emitCallbackVersionedLoop(
|
||||
emitter.callbackVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
|
||||
@@ -2062,6 +2062,20 @@ function emitCallbackVersionedLoop(
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
/** The copy of a versioned loop that runs when its guard fails. Its loops
|
||||
* get no speculative or int32 kind copies, which would emit each loop again
|
||||
* for a path that rarely runs. Proven integer loops, which emit one copy,
|
||||
* are kept. */
|
||||
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
const copies = emitter.loopCopiesEnabled;
|
||||
emitter.loopCopiesEnabled = false;
|
||||
try {
|
||||
emitter.emitStmt(s);
|
||||
} finally {
|
||||
emitter.loopCopiesEnabled = copies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entries in a row whose fast copy bailed before a speculative loop site
|
||||
* stops trying it. */
|
||||
const SPECULATION_BAILS = 8;
|
||||
@@ -2082,28 +2096,56 @@ 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;
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
|
||||
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
|
||||
if (version.counted?.guarded || version.paired?.loop.guarded)
|
||||
LOOPS.of(emitter).provenCountedGuards.add(s);
|
||||
B.startBlock(fast);
|
||||
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);
|
||||
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;
|
||||
LOOPS.of(emitter).provenCountedGuards.delete(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
|
||||
}
|
||||
@@ -2120,6 +2162,7 @@ export function emitLoopCopies(
|
||||
const plan =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planSpeculativeLoop(
|
||||
@@ -2136,6 +2179,7 @@ export function emitLoopCopies(
|
||||
);
|
||||
},
|
||||
(id) => emitter.speculativeLocal(id) || emitter.speculativeInvariantGlobal(id),
|
||||
emitter.speculationContext(),
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
@@ -2147,6 +2191,7 @@ export function emitLoopCopies(
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.loopCopiesEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
@@ -2193,6 +2238,20 @@ export function emitLoopCopies(
|
||||
B.line(`${both} = and i1 ${ok}, ${trying}`);
|
||||
ok = both;
|
||||
}
|
||||
// Hoisted leaves live in locals of their own, which the resume copy of
|
||||
// their write reads (see SpeculativeValue).
|
||||
for (const { leaves } of plan.values.values())
|
||||
for (const leaf of leaves) {
|
||||
emitter.currentLocals.set(leaf.localId, {
|
||||
id: leaf.localId,
|
||||
name: "speculated leaf",
|
||||
type: leaf.expr.type,
|
||||
mutable: true,
|
||||
});
|
||||
B.entryAllocas.push(
|
||||
`${emitter.binding(leaf.localId).slot} = alloca double ; speculated leaf`,
|
||||
);
|
||||
}
|
||||
const fast = B.newLabel("spec.fast"),
|
||||
slow = B.newLabel("spec.slow");
|
||||
B.condBr(ok, fast, slow);
|
||||
@@ -2218,6 +2277,7 @@ export function emitLoopCopies(
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header");
|
||||
B.startBlock(B.newLabel("spec.tail"));
|
||||
emitter.speculationResume = resume;
|
||||
emitter.speculationResumeValues = plan.values;
|
||||
emitter.speculationHeader = () => {
|
||||
B.br(header!);
|
||||
B.startBlock(B.newLabel("spec.unused"));
|
||||
@@ -2226,6 +2286,7 @@ export function emitLoopCopies(
|
||||
emitCopy(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationResumeValues = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
B.startBlock(fast);
|
||||
@@ -2274,6 +2335,7 @@ export function emitLoopCopies(
|
||||
deferred,
|
||||
bails,
|
||||
ran,
|
||||
values: plan.values,
|
||||
};
|
||||
try {
|
||||
const leave = B.newLabel("spec.leave");
|
||||
@@ -2531,9 +2593,22 @@ export function emitSpeculativeWrite(
|
||||
// at once: a bailing path then owns nothing, and reads have no effect
|
||||
// whose reordering before the arithmetic could be observed.
|
||||
const reads = new Map<IrExpr, LlValue>();
|
||||
if (target.value !== null) {
|
||||
const rewritten = emitter.speculation!.values?.get(s);
|
||||
const value = rewritten?.value ?? target.value;
|
||||
if (rewritten !== undefined) {
|
||||
// Hoisted leaves run first, in the write's order, and keep their
|
||||
// doubles for the resume copy.
|
||||
emitter.frames.push([]);
|
||||
for (const read of speculativeReads(target.value)) reads.set(read, emitter.emitExpr(read));
|
||||
for (const leaf of rewritten.leaves) {
|
||||
const evaluated = emitter.emitExpr(leaf.expr);
|
||||
B.line(`store double ${evaluated.name}, ptr ${emitter.binding(leaf.localId).slot}`);
|
||||
reads.set(leaf.ref, evaluated);
|
||||
}
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
}
|
||||
if (value !== null) {
|
||||
emitter.frames.push([]);
|
||||
for (const read of speculativeReads(value)) reads.set(read, emitter.emitExpr(read));
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
}
|
||||
emitter.frames.push([]);
|
||||
@@ -2548,7 +2623,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");
|
||||
|
||||
@@ -80,6 +80,8 @@ 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,
|
||||
@@ -109,6 +111,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,
|
||||
@@ -128,6 +132,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,
|
||||
@@ -343,6 +357,13 @@ 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,
|
||||
@@ -439,6 +460,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`)
|
||||
@@ -565,13 +592,17 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
pairRowSums(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -593,16 +624,36 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
@@ -612,7 +663,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]));
|
||||
@@ -1257,6 +1308,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. */
|
||||
@@ -1465,6 +1520,9 @@ export class LlEmitter {
|
||||
integerTempsEnabled = false;
|
||||
/** Speculative integer loops (speculative-integers.ts) run in this function. */
|
||||
speculativeLoopsEnabled = false;
|
||||
/** False inside the copy of a versioned loop that runs when its guard
|
||||
* fails: loops there get no speculative or int32 kind copies. */
|
||||
loopCopiesEnabled = true;
|
||||
/** The fast copy of a speculative integer loop being emitted: its
|
||||
* integer-computed writes and where each resumes in the ordinary copy. */
|
||||
speculation: {
|
||||
@@ -1477,11 +1535,47 @@ 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 module. */
|
||||
private 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. */
|
||||
@@ -1519,6 +1613,12 @@ 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();
|
||||
@@ -1716,6 +1816,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
|
||||
@@ -1851,6 +1961,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);
|
||||
@@ -2130,7 +2243,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
|
||||
@@ -3240,6 +3354,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 =
|
||||
@@ -3270,6 +3391,7 @@ export class LlEmitter {
|
||||
passThrough !== undefined &&
|
||||
!target &&
|
||||
this.currentCoro === null &&
|
||||
!this.currentUnwindAbi &&
|
||||
!this.currentInt32Result
|
||||
) {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
@@ -3456,6 +3578,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 {
|
||||
@@ -4372,11 +4534,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);
|
||||
@@ -5219,7 +5391,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).
|
||||
@@ -5245,7 +5432,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);
|
||||
@@ -5658,6 +5847,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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,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 +323,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 +374,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 +391,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);
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
@@ -843,8 +843,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
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
@@ -649,6 +649,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 +936,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.
|
||||
|
||||
@@ -119,6 +119,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
B.line(`call void @scr_child_on_error(ptr ${child.name}, ptr ${cb.name}, ptr @${adapter})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "child.hasChannel") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_present(ptr)`);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call i1 @scr_child_ipc_present(ptr ${child.name})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (e.fn === "child.connected") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_connected(ptr)`);
|
||||
|
||||
@@ -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",
|
||||
@@ -795,6 +804,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",
|
||||
@@ -989,6 +999,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",
|
||||
|
||||
@@ -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,295 @@ 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;
|
||||
}
|
||||
|
||||
/** 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 +460,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 +488,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 +497,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 +532,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 +562,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 +578,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 +618,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 +630,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 +661,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 +699,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 +712,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 +725,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
|
||||
@@ -682,6 +1005,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 +1035,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(
|
||||
|
||||
@@ -75,6 +75,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 +127,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} }`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,13 +4,15 @@ import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-rang
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { LlInteger } from "./integer-values.js";
|
||||
|
||||
/** Marks the `+` and `-` operands whose number only reaches ToInt32/ToUint32
|
||||
* consumers (`| 0`, the other bitwise operators, `Math.imul`, `Math.clz32`),
|
||||
* directly or through further sums, in the emitter's `wrappingSums`. Called
|
||||
* by the consumer before its operands emit. */
|
||||
/** Marks the `+`, `-` and `*` operands whose number only reaches
|
||||
* ToInt32/ToUint32 consumers (`| 0`, the other bitwise operators,
|
||||
* `Math.imul`, `Math.clz32`), directly or through further sums and
|
||||
* products, in the emitter's `wrappingSums`. Called by the consumer before
|
||||
* its operands emit. */
|
||||
export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrExpr[]): void {
|
||||
for (const operand of operands) {
|
||||
if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-")) continue;
|
||||
if (operand.kind !== "bin" || (operand.op !== "+" && operand.op !== "-" && operand.op !== "*"))
|
||||
continue;
|
||||
if (operand.type.kind !== "f64" || host.wrappingSums.has(operand)) continue;
|
||||
host.wrappingSums.add(operand);
|
||||
markWrappingOperands(host, operand.left, operand.right);
|
||||
@@ -128,12 +130,15 @@ function wrappingOperand(
|
||||
return { low, valid };
|
||||
}
|
||||
|
||||
/** ToUint32 of `left op right` (`+` or `-`) in 32-bit arithmetic when the
|
||||
* expression only reaches ToInt32/ToUint32 consumers and both operands are
|
||||
* exact integers or NaN with an exact result. Both conversions are
|
||||
* arithmetic mod 2^32, so the wrapping i32 sum of the operands' low halves
|
||||
* is the converted exact sum; a NaN operand makes the sum NaN, which
|
||||
* converts to 0. Returns undefined, emitting nothing, for any other sum. */
|
||||
/** ToUint32 of `left op right` (`+`, `-` or `*`) in 32-bit arithmetic when
|
||||
* the expression only reaches ToInt32/ToUint32 consumers and both operands
|
||||
* are exact integers or NaN with an exact result. Both conversions are
|
||||
* arithmetic mod 2^32, so the wrapping i32 sum or product of the operands'
|
||||
* low halves is the converted exact result; a NaN operand makes the result
|
||||
* NaN, which converts to 0. A product is exact when the operands' magnitude
|
||||
* bounds multiply to at most 2^53; it may be -0 (`0 * -1`), which converts
|
||||
* to 0 like the integer 0. Returns undefined, emitting nothing, for any
|
||||
* other expression. */
|
||||
export function emitWrappingSum(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "bin" },
|
||||
@@ -142,15 +147,23 @@ export function emitWrappingSum(
|
||||
li: LlInteger | null,
|
||||
ri: LlInteger | null,
|
||||
): string | undefined {
|
||||
if ((e.op !== "+" && e.op !== "-") || !host.wrappingSums.has(e)) return undefined;
|
||||
if ((e.op !== "+" && e.op !== "-" && e.op !== "*") || !host.wrappingSums.has(e)) return undefined;
|
||||
const leftRange = li ? li.range : integerOrNaNRange(host, e.left);
|
||||
const rightRange = leftRange && (ri ? ri.range : integerOrNaNRange(host, e.right));
|
||||
if (!rightRange || !integerArithmeticRange(e.op, leftRange!, rightRange)) return undefined;
|
||||
if (!rightRange) return undefined;
|
||||
if (
|
||||
e.op === "*"
|
||||
? !exactProduct(leftRange!, rightRange)
|
||||
: !integerArithmeticRange(e.op, leftRange!, rightRange)
|
||||
)
|
||||
return undefined;
|
||||
const B = host.B;
|
||||
const left = wrappingOperand(host, l, li, leftRange!);
|
||||
const right = wrappingOperand(host, r, ri, rightRange);
|
||||
const sum = B.tmp();
|
||||
B.line(`${sum} = ${e.op === "+" ? "add" : "sub"} i32 ${left.low}, ${right.low}`);
|
||||
B.line(
|
||||
`${sum} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i32 ${left.low}, ${right.low}`,
|
||||
);
|
||||
let valid = left.valid ?? right.valid;
|
||||
if (valid === null) return sum;
|
||||
if (left.valid && right.valid) {
|
||||
@@ -162,6 +175,17 @@ export function emitWrappingSum(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether every product of a value in `left` and one in `right` is an
|
||||
* integer of magnitude below 2^53, which the double product holds exactly.
|
||||
* The bound's own product is computed in a double: a bound of at least
|
||||
* 2^53 never rounds below 2^53, while 2^53 + 1 (3 * 3002399751580331)
|
||||
* rounds to 2^53, so the test is strict. */
|
||||
function exactProduct(left: IntegerRange, right: IntegerRange): boolean {
|
||||
const magnitude = (range: IntegerRange): number =>
|
||||
Math.max(Math.abs(range.min), Math.abs(range.max));
|
||||
return magnitude(left) * magnitude(right) < 2 ** 53;
|
||||
}
|
||||
|
||||
/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of
|
||||
* those integers: locals declared once with an initializer, neither parameters nor captured or
|
||||
* boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once
|
||||
|
||||
@@ -30,6 +30,8 @@ import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import {
|
||||
nativeProfileHelperArgs,
|
||||
nativeProfileKey,
|
||||
parseNativeProfileMatch,
|
||||
type NativeProfileMatch,
|
||||
type NativeProfileOptions,
|
||||
} from "./native-profile.js";
|
||||
import { writeTextParts } from "../text-parts.js";
|
||||
@@ -69,6 +71,8 @@ export interface NativeCodegenArtifact {
|
||||
dependencies: NativeArtifactDependency[];
|
||||
/** Every emitted file, starting with the requested output path. */
|
||||
outputPaths: string[];
|
||||
/** How the build's profile matched, when it used one and the helper ran. */
|
||||
profileMatch?: NativeProfileMatch;
|
||||
}
|
||||
|
||||
const resolvedHelperCache = new Map<string, Promise<ResolvedHelper>>();
|
||||
@@ -458,7 +462,7 @@ export async function emitNativeArtifact(
|
||||
try {
|
||||
if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input);
|
||||
else await writeLlvmSource(input, options.llvm, 0o600);
|
||||
await invoke(helper.binaryPath, [
|
||||
const { stdout } = await invoke(helper.binaryPath, [
|
||||
"emit",
|
||||
"--input",
|
||||
input,
|
||||
@@ -502,7 +506,9 @@ export async function emitNativeArtifact(
|
||||
for (let index = 0; index < stages.length; index++)
|
||||
await rename(stages[index]!, outputs[index]!);
|
||||
await pruneBuildCache(root);
|
||||
return artifact;
|
||||
const profileMatch =
|
||||
options.profile?.use === undefined ? null : parseNativeProfileMatch(stdout);
|
||||
return profileMatch === null ? artifact : { ...artifact, profileMatch };
|
||||
} finally {
|
||||
await Promise.all([
|
||||
...stages.map((stage) => rm(stage, { force: true }).catch(() => undefined)),
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface NativeLinkFeatures {
|
||||
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
|
||||
* SCR_NODE_ISLANDS variant of the event loop unit. */
|
||||
nodeIsland?: boolean;
|
||||
/** Module namespace objects as checked-dynamic values link
|
||||
* scr_module_namespace.c; absent (false) otherwise. */
|
||||
moduleNamespace?: boolean;
|
||||
zlib: boolean;
|
||||
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { executableLinkInputs } from "./link-plan-core.js";
|
||||
import {
|
||||
parseNativeProfileMatch,
|
||||
nativeProfileHelperArgs,
|
||||
nativeProfileKey,
|
||||
nativeProfileRefusal,
|
||||
@@ -71,3 +72,15 @@ test("only the instrumented link adds the profile runtime", () => {
|
||||
"-fprofile-generate",
|
||||
);
|
||||
});
|
||||
|
||||
test("the helper's profile match is read from its output", () => {
|
||||
expect(
|
||||
parseNativeProfileMatch(
|
||||
'{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"unknown_partitions":1}}\n',
|
||||
),
|
||||
).toEqual({ annotated: 120, mismatched: 7, missing: 40, unknownPartitions: 1 });
|
||||
// A helper that predates the report prints nothing.
|
||||
expect(parseNativeProfileMatch("")).toBeNull();
|
||||
expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull();
|
||||
expect(parseNativeProfileMatch('{"ok":true,"profile":{"annotated":-1}}\n')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -44,6 +44,45 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine
|
||||
return [];
|
||||
}
|
||||
|
||||
/** How an optimized build's profile matched the program's functions, as the
|
||||
* helper reports it: functions that took the profile's counts, functions
|
||||
* whose control flow changed since the profile was collected, functions the
|
||||
* profile has no record of (new, or never run while it was collected), and
|
||||
* partitions reused from the cache without their counts. */
|
||||
export interface NativeProfileMatch {
|
||||
annotated: number;
|
||||
mismatched: number;
|
||||
missing: number;
|
||||
unknownPartitions: number;
|
||||
}
|
||||
|
||||
/** The profile match in a successful helper emit's output, or null when the
|
||||
* helper reports none (a helper that predates the report). */
|
||||
export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | null {
|
||||
for (const line of stdout.split("\n")) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const profile = (parsed as { profile?: Record<string, unknown> } | null)?.profile;
|
||||
if (typeof profile !== "object" || profile === null) continue;
|
||||
const count = (key: string): number | null => {
|
||||
const value = profile[key];
|
||||
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
||||
};
|
||||
const annotated = count("annotated");
|
||||
const mismatched = count("mismatched");
|
||||
const missing = count("missing");
|
||||
const unknownPartitions = count("unknown_partitions");
|
||||
if (annotated === null || mismatched === null || missing === null || unknownPartitions === null)
|
||||
continue;
|
||||
return { annotated, mismatched, missing, unknownPartitions };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Cache-key part; empty without a profile so other keys are unchanged. */
|
||||
export function nativeProfileKey(profile: NativeProfileOptions | undefined): string {
|
||||
if (profile?.generate !== undefined) return `profile-generate:${profile.generate}`;
|
||||
|
||||
@@ -150,6 +150,9 @@ export interface CcOptions {
|
||||
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
|
||||
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
|
||||
nodeIsland?: boolean;
|
||||
/** Module namespace objects as checked-dynamic values: compiles
|
||||
* scr_module_namespace.c. */
|
||||
moduleNamespace?: boolean;
|
||||
/** The program uses the process-events surface (signal/exit listeners,
|
||||
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
|
||||
* IR): compiles scr_events.c into the binary. Event-free binaries keep
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
cacheTargetIdentity,
|
||||
isZigDriver,
|
||||
isMuslTarget,
|
||||
type CcDriver,
|
||||
} from "./driver.js";
|
||||
import {
|
||||
resolveProgramShardMergeIdentity,
|
||||
@@ -194,6 +195,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"),
|
||||
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
|
||||
unit((u) => u.opts.wasm, "scr_wasm.c"),
|
||||
// Module namespace objects as checked-dynamic values.
|
||||
unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynAsync ||
|
||||
@@ -823,6 +826,7 @@ export async function compileCInternal(
|
||||
// generated/native input that references inflate symbols. Cross
|
||||
// and targetless Zig builds use vendored zlib objects in the input section above.
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
// glibc keeps libm separate from libc. This must trail the generated
|
||||
// program and every native FFI input because GNU ld resolves archives
|
||||
// from left to right.
|
||||
@@ -1070,6 +1074,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1082,6 +1087,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1691,3 +1697,11 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Linux runtimes raise through the native unwinder (scr_unwind_raise):
|
||||
* Zig's libunwind, or the toolchain's libgcc_eh. Both are static archives,
|
||||
* so programs load no shared library for them. */
|
||||
function unwinderLinkArgs(driver: CcDriver): string[] {
|
||||
if (targetPlatform(driver) !== "linux") return [];
|
||||
return isZigDriver(driver) ? ["-lunwind"] : ["-lgcc_eh"];
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_object.c",
|
||||
"scr_union.c",
|
||||
"scr_exception.c",
|
||||
"scr_unwind.c",
|
||||
"scr_error.c",
|
||||
"scr_console.c",
|
||||
"scr_lib.c",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 37 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const;
|
||||
export const RUNTIME_ABI_VERSION = 42 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v42" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
stdioExtended: boolean;
|
||||
nodeIsland: boolean;
|
||||
brotli: boolean;
|
||||
moduleNamespace: boolean;
|
||||
nativeFetch: boolean;
|
||||
netEffective: boolean;
|
||||
httpEffective: boolean;
|
||||
@@ -295,6 +296,7 @@ export function effectiveRuntimeFeatures(
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
nodeIsland: features.nodeIsland === true,
|
||||
brotli: features.brotli === true,
|
||||
moduleNamespace: features.moduleNamespace === true,
|
||||
nativeFetch,
|
||||
netEffective,
|
||||
httpEffective,
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { nativeCodegenDiag } from "../diagnostics/diagnostic.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { RuntimePackError } from "./runtime-pack-core.js";
|
||||
import { selectNativeRuntimePack } from "./runtime-pack-native.js";
|
||||
import { WASM32_WASI_TARGET } from "./targets.js";
|
||||
import { RuntimePackError, type RuntimePackManifest } from "./runtime-pack-core.js";
|
||||
import {
|
||||
runtimePackManifest,
|
||||
selectNativeRuntimePack,
|
||||
stageNativeRuntimeSelection,
|
||||
type NativeRuntimeSelection,
|
||||
type RuntimePackStore,
|
||||
} from "./runtime-pack-native.js";
|
||||
import { MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js";
|
||||
|
||||
vi.mock("node:crypto", async (original) => {
|
||||
const crypto = await original<typeof import("node:crypto")>();
|
||||
return { ...crypto, createHash: vi.fn(crypto.createHash) };
|
||||
});
|
||||
vi.mock("node:fs", async (original) => {
|
||||
const fs = await original<typeof import("node:fs")>();
|
||||
return { ...fs, readFileSync: vi.fn(fs.readFileSync) };
|
||||
});
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterEach(async () =>
|
||||
@@ -45,3 +72,171 @@ test("a missing native helper keeps the reinstall hint", () => {
|
||||
"reinstall scriptc with optional dependencies enabled for this host",
|
||||
);
|
||||
});
|
||||
|
||||
/** A runtime pack with one runtime object and a license, and a store that
|
||||
* keeps entries in memory. */
|
||||
async function packFixture() {
|
||||
const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-store-"));
|
||||
dirs.push(root);
|
||||
const target = MACOS_ARM64_TARGET;
|
||||
const version = compilerReleaseVersion();
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ name: target.runtimePackPackage, version }),
|
||||
);
|
||||
mkdirSync(join(root, "artifacts"));
|
||||
const object = join(root, "artifacts", "base.o");
|
||||
writeFileSync(object, "base object");
|
||||
writeFileSync(join(root, "license.txt"), "license");
|
||||
const unit = {
|
||||
source: "scr_base.c",
|
||||
predicate: true,
|
||||
variants: [
|
||||
{
|
||||
id: "default",
|
||||
when: {},
|
||||
defines: [],
|
||||
path: "artifacts/base.o",
|
||||
sha256: createHash("sha256").update("base object").digest("hex"),
|
||||
size: Buffer.byteLength("base object"),
|
||||
},
|
||||
],
|
||||
};
|
||||
const manifest: RuntimePackManifest = {
|
||||
schema: "scriptc.runtime-pack.v1",
|
||||
format: 1,
|
||||
package: target.runtimePackPackage,
|
||||
version,
|
||||
target: {
|
||||
name: target.name,
|
||||
llvm_triple: target.llvmTriple,
|
||||
architecture: target.architecture,
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
compiler: { command: "clang", identity: "fixture clang", target: target.llvmTriple },
|
||||
macros: {
|
||||
executable: [],
|
||||
excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT"],
|
||||
sanitizer: "external-toolchain-required",
|
||||
},
|
||||
flavors: {
|
||||
release: { optimization: "-O2", runtime_units: [unit] },
|
||||
dev: { optimization: "-O0", runtime_units: [unit] },
|
||||
},
|
||||
archives: [],
|
||||
system_libraries: [{ name: "System", predicate: true }],
|
||||
licenses: [{ path: "license.txt", license: "fixture" }],
|
||||
};
|
||||
const manifestText = JSON.stringify(manifest);
|
||||
writeFileSync(join(root, "runtime-pack.json"), manifestText);
|
||||
const entries = new Map<string, string>();
|
||||
const store: RuntimePackStore = {
|
||||
read: (key) => entries.get(key) ?? null,
|
||||
write: (key, text) => void entries.set(key, text),
|
||||
};
|
||||
const select = () =>
|
||||
selectNativeRuntimePack(
|
||||
root,
|
||||
target,
|
||||
version,
|
||||
{} as NativeLinkFeatures,
|
||||
"release",
|
||||
"executable",
|
||||
store,
|
||||
);
|
||||
let stages = 0;
|
||||
const stage = (selection: NativeRuntimeSelection) => {
|
||||
const directory = join(root, `stage-${stages++}`);
|
||||
mkdirSync(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, store);
|
||||
};
|
||||
return { root, object, manifestText, entries, select, stage };
|
||||
}
|
||||
|
||||
test("a stored selection answers while the pack's files keep their identities", async () => {
|
||||
const f = await packFixture();
|
||||
const first = f.select();
|
||||
expect(first.selected.systemLibraries).toEqual(["System"]);
|
||||
expect(f.entries.size).toBe(1);
|
||||
// The next selection reads the stored answer instead of the manifest.
|
||||
const [key, text] = [...f.entries][0]!;
|
||||
const stored = JSON.parse(text);
|
||||
stored.selected.systemLibraries = ["Stored"];
|
||||
f.entries.set(key, JSON.stringify(stored));
|
||||
const second = f.select();
|
||||
expect(second.selected.systemLibraries).toEqual(["Stored"]);
|
||||
expect(second.licenses).toEqual(first.licenses);
|
||||
expect(second.manifest).toBeUndefined();
|
||||
expect(runtimePackManifest(second).flavors.release).toEqual(first.manifest!.flavors.release);
|
||||
// Writing the manifest again, with the same text, gives it a new identity.
|
||||
writeFileSync(join(f.root, "runtime-pack.json"), f.manifestText);
|
||||
expect(f.select().selected.systemLibraries).toEqual(["System"]);
|
||||
});
|
||||
|
||||
test.each(["replaced", "rewritten in place"])(
|
||||
"staging hashes an artifact again when it is %s, even at the same size and mtime",
|
||||
async (change) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
const changed = change === "replaced" ? f.object + ".new" : f.object;
|
||||
writeFileSync(changed, "x".repeat(before.size));
|
||||
utimesSync(changed, before.atime, before.mtime);
|
||||
if (changed !== f.object) renameSync(changed, f.object);
|
||||
expect(() => f.stage(selection)).toThrow(
|
||||
"runtime pack artifact hash mismatch: artifacts/base.o",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("staging does not hash an unchanged artifact again", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
const hashes = () => vi.mocked(createHash).mock.calls.length;
|
||||
let start = hashes();
|
||||
f.stage(selection);
|
||||
const firstStage = hashes() - start;
|
||||
start = hashes();
|
||||
const staged = f.stage(selection);
|
||||
// Only the store key is hashed; the staged copy has the verified bytes.
|
||||
expect(hashes() - start).toBe(firstStage - 1);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"staging reads an artifact's bytes from the file whose identity it checks",
|
||||
async () => {
|
||||
const f = await packFixture();
|
||||
// The artifact path is a symbolic link to the verified file.
|
||||
const real = f.object + ".real";
|
||||
renameSync(f.object, real);
|
||||
symlinkSync(real, f.object);
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
// During the next read the link names another file of the same size,
|
||||
// and it names the verified file again before the read ends.
|
||||
const other = f.object + ".other";
|
||||
writeFileSync(other, "x".repeat(statSync(real).size));
|
||||
const read = vi.mocked(readFileSync);
|
||||
const original = read.getMockImplementation()!;
|
||||
read.mockImplementation(((source: Parameters<typeof readFileSync>[0], ...rest: unknown[]) => {
|
||||
if (typeof source === "string" && !source.endsWith("base.o"))
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
read.mockImplementation(original);
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(other, f.object);
|
||||
try {
|
||||
return (original as (...args: unknown[]) => unknown)(source, ...rest);
|
||||
} finally {
|
||||
unlinkSync(f.object);
|
||||
symlinkSync(real, f.object);
|
||||
}
|
||||
}) as typeof readFileSync);
|
||||
const staged = f.stage(selection);
|
||||
expect(read.getMockImplementation()).toBe(original);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
/** Synchronous runtime-pack host for the statically compiled driver. Each
|
||||
* selected artifact is hashed and copied from the same read into private
|
||||
* storage. This host has no persistent cache or mutable installed link inputs. */
|
||||
* selected artifact is verified and copied from the same read into private
|
||||
* storage. With a store, a pack's selections and the identities of the
|
||||
* artifacts it verified are kept between builds: an artifact whose file
|
||||
* identity is unchanged since it was hashed is not hashed again. */
|
||||
import type { NativeOptimization } from "./optimization.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
constants,
|
||||
existsSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
writeSync,
|
||||
type Stats,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
@@ -34,13 +39,66 @@ export interface NativeRuntimePack {
|
||||
|
||||
export interface NativeRuntimeSelection {
|
||||
root: string;
|
||||
manifest: RuntimePackManifest;
|
||||
/** The parsed manifest; a selection read from a store has none until
|
||||
* runtimePackManifest parses it. */
|
||||
manifest?: RuntimePackManifest;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
packageText: string;
|
||||
manifestText: string;
|
||||
selected: RuntimePackArtifacts;
|
||||
flavor: NativeOptimization;
|
||||
}
|
||||
|
||||
/** Text entries kept between builds (the native compiler's cache). */
|
||||
export interface RuntimePackStore {
|
||||
read(key: string): string | null;
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
interface FileIdentity {
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
}
|
||||
|
||||
function fileIdentity(path: string): FileIdentity {
|
||||
return statsIdentity(statSync(path));
|
||||
}
|
||||
|
||||
function statsIdentity(info: Stats): FileIdentity {
|
||||
return {
|
||||
dev: info.dev,
|
||||
ino: info.ino,
|
||||
size: info.size,
|
||||
mtime: info.mtimeMs,
|
||||
ctime: info.ctimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
function sameIdentity(a: FileIdentity, b: FileIdentity): boolean {
|
||||
return (
|
||||
a.dev === b.dev &&
|
||||
a.ino === b.ino &&
|
||||
a.size === b.size &&
|
||||
a.mtime === b.mtime &&
|
||||
a.ctime === b.ctime
|
||||
);
|
||||
}
|
||||
|
||||
function storeKey(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
/** A stored selection: what selecting from the pack's files, with these
|
||||
* identities, returned. */
|
||||
interface StoredSelection {
|
||||
schema: 1;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
selected: RuntimePackArtifacts;
|
||||
}
|
||||
|
||||
export function selectNativeRuntimePack(
|
||||
root: string,
|
||||
target: NativeTargetSpec,
|
||||
@@ -48,38 +106,204 @@ export function selectNativeRuntimePack(
|
||||
features: NativeLinkFeatures,
|
||||
flavor: NativeOptimization,
|
||||
mode: RuntimePackMode = "executable",
|
||||
store?: RuntimePackStore,
|
||||
): NativeRuntimeSelection {
|
||||
if (!existsSync(join(root, "package.json"))) {
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
if (!existsSync(packagePath)) {
|
||||
throw new RuntimePackError(
|
||||
`runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`,
|
||||
"missing",
|
||||
"scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project",
|
||||
);
|
||||
}
|
||||
const packageText = readFileSync(join(root, "package.json"), "utf8");
|
||||
const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8");
|
||||
// The pack's files by identity, with everything else selection reads.
|
||||
let key: string | null = null;
|
||||
let identities: FileIdentity[] = [];
|
||||
if (store !== undefined) {
|
||||
try {
|
||||
identities = [fileIdentity(packagePath), fileIdentity(manifestPath)];
|
||||
key = storeKey(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
root,
|
||||
identities,
|
||||
target: target.name,
|
||||
compilerVersion,
|
||||
features,
|
||||
flavor,
|
||||
mode,
|
||||
}),
|
||||
);
|
||||
const stored = readStoredSelection(store.read(key));
|
||||
if (stored !== null) {
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
// The texts read are the ones with the stored identities.
|
||||
if (
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
return {
|
||||
root,
|
||||
licenses: stored.licenses,
|
||||
packageText,
|
||||
manifestText,
|
||||
selected: stored.selected,
|
||||
flavor,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
const manifest = parseRuntimePackManifest(JSON.parse(manifestText));
|
||||
validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, features, flavor, process.env, mode);
|
||||
return { root, manifest, packageText, manifestText, selected, flavor };
|
||||
if (store !== undefined && key !== null) {
|
||||
try {
|
||||
// Stored only when the files selected from still have the identities
|
||||
// in the key.
|
||||
if (
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected };
|
||||
store.write(key, JSON.stringify(entry));
|
||||
}
|
||||
} catch {
|
||||
/* A failed write is a later miss. */
|
||||
}
|
||||
}
|
||||
return {
|
||||
root,
|
||||
manifest,
|
||||
licenses: manifest.licenses,
|
||||
packageText,
|
||||
manifestText,
|
||||
selected,
|
||||
flavor,
|
||||
};
|
||||
}
|
||||
|
||||
function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string {
|
||||
/** The selection's parsed manifest. */
|
||||
export function runtimePackManifest(selection: NativeRuntimeSelection): RuntimePackManifest {
|
||||
return (selection.manifest ??= parseRuntimePackManifest(JSON.parse(selection.manifestText)));
|
||||
}
|
||||
|
||||
function readStoredSelection(text: string | null): StoredSelection | null {
|
||||
if (text === null) return null;
|
||||
const value: unknown = JSON.parse(text);
|
||||
if (value === null || typeof value !== "object") return null;
|
||||
const entry = value as Partial<StoredSelection>;
|
||||
if (
|
||||
entry.schema !== 1 ||
|
||||
!Array.isArray(entry.licenses) ||
|
||||
entry.selected === undefined ||
|
||||
entry.selected === null ||
|
||||
typeof entry.selected !== "object" ||
|
||||
!Array.isArray(entry.selected.runtime) ||
|
||||
!Array.isArray(entry.selected.archives) ||
|
||||
!Array.isArray(entry.selected.systemLibraries)
|
||||
)
|
||||
return null;
|
||||
return entry as StoredSelection;
|
||||
}
|
||||
|
||||
/** An artifact path's identity when its bytes last matched the manifest's
|
||||
* size and digest. */
|
||||
interface VerifiedArtifact {
|
||||
path: string;
|
||||
sha256: string;
|
||||
dev: number;
|
||||
ino: number;
|
||||
size: number;
|
||||
mtime: number;
|
||||
ctime: number;
|
||||
}
|
||||
|
||||
interface StoredVerification {
|
||||
schema: number;
|
||||
artifacts: VerifiedArtifact[];
|
||||
}
|
||||
|
||||
function readVerified(
|
||||
store: RuntimePackStore | undefined,
|
||||
key: string,
|
||||
): Map<string, VerifiedArtifact> {
|
||||
const verified = new Map<string, VerifiedArtifact>();
|
||||
try {
|
||||
const text = store?.read(key) ?? null;
|
||||
if (text === null) return verified;
|
||||
const stored = JSON.parse(text) as StoredVerification;
|
||||
if (stored.schema === 1)
|
||||
for (const artifact of stored.artifacts) verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* A malformed entry verifies everything again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
function stageArtifact(
|
||||
root: string,
|
||||
stage: string,
|
||||
artifact: RuntimePackArtifact,
|
||||
verified: Map<string, VerifiedArtifact>,
|
||||
newlyVerified: VerifiedArtifact[],
|
||||
): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
let bytes: Buffer;
|
||||
let before: FileIdentity;
|
||||
let after: FileIdentity;
|
||||
try {
|
||||
bytes = readFileSync(source);
|
||||
// The identities are those of the open file the bytes come from, also
|
||||
// when the path is a symbolic link that changes during the read.
|
||||
const input = openSync(source, constants.O_RDONLY);
|
||||
try {
|
||||
before = statsIdentity(fstatSync(input));
|
||||
bytes = readFileSync(input);
|
||||
after = statsIdentity(fstatSync(input));
|
||||
} finally {
|
||||
closeSync(input);
|
||||
}
|
||||
} catch {
|
||||
throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid");
|
||||
}
|
||||
// The bytes are those last hashed when the file kept the identity it had
|
||||
// then for the whole read.
|
||||
const known = verified.get(artifact.path);
|
||||
const unchanged = sameIdentity(before, after);
|
||||
const trusted =
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
unchanged &&
|
||||
known.dev === before.dev &&
|
||||
known.ino === before.ino &&
|
||||
known.size === before.size &&
|
||||
known.mtime === before.mtime &&
|
||||
known.ctime === before.ctime;
|
||||
if (
|
||||
bytes.length !== artifact.size ||
|
||||
createHash("sha256").update(bytes).digest("hex") !== artifact.sha256
|
||||
(!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
||||
) {
|
||||
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
||||
}
|
||||
if (!trusted && unchanged)
|
||||
newlyVerified.push({
|
||||
path: artifact.path,
|
||||
sha256: artifact.sha256,
|
||||
dev: before.dev,
|
||||
ino: before.ino,
|
||||
size: before.size,
|
||||
mtime: before.mtime,
|
||||
ctime: before.ctime,
|
||||
});
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
const fd = openSync(
|
||||
destination,
|
||||
@@ -123,15 +347,19 @@ export function stageNativeRuntimePack(
|
||||
export function stageNativeRuntimeSelection(
|
||||
selection: NativeRuntimeSelection,
|
||||
stageRoot: string,
|
||||
store?: RuntimePackStore,
|
||||
): NativeRuntimePack {
|
||||
const { root, manifest, selected, packageText, manifestText } = selection;
|
||||
const { root, licenses, selected, packageText, manifestText } = selection;
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const runtimeObjects = selected.runtime.map((artifact) =>
|
||||
stageArtifact(root, stageRoot, artifact),
|
||||
);
|
||||
const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
for (const license of manifest.licenses) {
|
||||
const key = storeKey(JSON.stringify({ schema: 1, verified: root }));
|
||||
const verified = readVerified(store, key);
|
||||
const newlyVerified: VerifiedArtifact[] = [];
|
||||
const stage = (artifact: RuntimePackArtifact): string =>
|
||||
stageArtifact(root, stageRoot, artifact, verified, newlyVerified);
|
||||
const runtimeObjects = selected.runtime.map(stage);
|
||||
const archives = selected.archives.map(stage);
|
||||
for (const license of licenses) {
|
||||
try {
|
||||
readFileSync(join(root, license.path));
|
||||
} catch {
|
||||
@@ -144,5 +372,14 @@ export function stageNativeRuntimeSelection(
|
||||
) {
|
||||
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
||||
}
|
||||
if (store !== undefined && newlyVerified.length > 0) {
|
||||
for (const artifact of newlyVerified) verified.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] };
|
||||
try {
|
||||
store.write(key, JSON.stringify(entry));
|
||||
} catch {
|
||||
/* A failed write only means hashing again next time. */
|
||||
}
|
||||
}
|
||||
return { runtimeObjects, archives, systemLibraries: selected.systemLibraries };
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -128,7 +128,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
runtime_abi: { version: 42, marker: "scr_runtime_abi_v42" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TargetCpu } from "./backend/target-cpu.js";
|
||||
import type { WindowsSubsystem } from "./backend/targets.js";
|
||||
import type { NativeLinkInfo } from "./backend/native-link-info.js";
|
||||
import type { NativeOptimization } from "./backend/optimization.js";
|
||||
import type { NativeProfileMatch } from "./backend/native-profile.js";
|
||||
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
import type { TierMode } from "./frontend/tier-analysis.js";
|
||||
@@ -106,6 +107,8 @@ export type CompileArtifact =
|
||||
path: string;
|
||||
translationUnitPath: string;
|
||||
backend: "llvm";
|
||||
/** How a `profileUse` build's profile matched the program's functions. */
|
||||
profileMatch?: NativeProfileMatch;
|
||||
};
|
||||
|
||||
export type CompileFailure = {
|
||||
|
||||
@@ -648,7 +648,7 @@ export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnosti
|
||||
code: "SC2001",
|
||||
message:
|
||||
`values of type '${typeText}' cannot be compiled yet ` +
|
||||
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')`,
|
||||
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
warningReplace?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
zlib: boolean;
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DYN,
|
||||
DYN_HANDLE_KINDS,
|
||||
F64,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
@@ -285,6 +286,43 @@ export function lowerAbstractEquality(
|
||||
};
|
||||
return negated ? { kind: "unary", op: "!", operand: result, type: BOOL, loc } : result;
|
||||
}
|
||||
// Two Dates are equal when they are the same object, as with ===.
|
||||
if (left.type.kind === "date" && right.type.kind === "date")
|
||||
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
||||
// A Date and a string or number compare through the Date's default
|
||||
// primitive, its toString() text.
|
||||
if (left.type.kind === "date" || right.type.kind === "date") {
|
||||
const otherType = left.type.kind === "date" ? right.type : left.type;
|
||||
if (otherType.kind === "string" || otherType.kind === "f64") {
|
||||
// Both operands evaluate before the Date converts.
|
||||
const l = lowerer.declareHiddenLocal("%looseA", left.type);
|
||||
const r = lowerer.declareHiddenLocal("%looseB", right.type);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: l.id, init: left, loc },
|
||||
{ kind: "varDecl", localId: r.id, init: right, loc },
|
||||
];
|
||||
const primitive = (value: IrExpr): IrExpr => {
|
||||
if (value.type.kind !== "date") return value;
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "date.toString",
|
||||
args: [value],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return otherType.kind === "string"
|
||||
? text
|
||||
: { kind: "libCall", fn: "num.fromString", args: [text], type: F64, loc };
|
||||
};
|
||||
const a = primitive(varRef(l.id, left.type, loc));
|
||||
const b = primitive(varRef(r.id, right.type, loc));
|
||||
const result: IrExpr =
|
||||
otherType.kind === "string"
|
||||
? { kind: "strEq", negated, left: a, right: b, type: BOOL, loc }
|
||||
: { kind: "bin", op: negated ? "!==" : "===", left: a, right: b, type: BOOL, loc };
|
||||
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
if (!supportsPair(lowerer, left.type, right.type)) return null;
|
||||
if (left.kind === "strLit" && right.kind === "strLit") {
|
||||
return { kind: "boolLit", value: (left.value === right.value) !== negated, type: BOOL, loc };
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { BOOL, F64, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isThisParameter } from "./call-signatures.js";
|
||||
import { buildFunctionAdapter } from "./coercions/builders.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
|
||||
/** `arguments.length` in a top-level function declaration that the program
|
||||
* also uses as a value. A direct call passes the declared parameters (tsc
|
||||
* checks the arity), while a call through the value passes the parameter
|
||||
* count of the function type the value was passed as (`[1].map(f)` calls f
|
||||
* with 3). The value is an adapter that stores that count in a global slot
|
||||
* before forwarding; the function reads the slot on entry and clears it,
|
||||
* so a direct call (the cleared slot) sees the declared count. */
|
||||
|
||||
/** The count slot of `decl`, or null when its `arguments.length` folds to
|
||||
* the declared count (or the declaration does not qualify). */
|
||||
export function argumentCountSlot(lowerer: Lowerer, decl: ts.FunctionDeclaration): string | null {
|
||||
const cached = lowerer.argumentCountSlots.get(decl);
|
||||
if (cached !== undefined) return cached;
|
||||
let slot: string | null = null;
|
||||
if (
|
||||
decl.body !== undefined &&
|
||||
decl.name !== undefined &&
|
||||
decl.asteriskToken === undefined &&
|
||||
!decl.typeParameters?.nodes.length &&
|
||||
lowerer.isTopLevelFnSymbol(decl.name) &&
|
||||
decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
fixedArity(decl) &&
|
||||
bodyReadsArguments(decl) &&
|
||||
usedAsValue(lowerer, decl)
|
||||
) {
|
||||
const sig = lowerer.fnSigOf(decl.name);
|
||||
if (sig) {
|
||||
slot = `%g.argc.${sig.name}`;
|
||||
lowerer.globalsList.push({ id: slot, name: "%argc", type: F64, mutable: true });
|
||||
}
|
||||
}
|
||||
lowerer.argumentCountSlots.set(decl, slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
/** Reads `decl`'s count slot into a hidden local on entry and clears it; the
|
||||
* local holds the declared count when the slot was clear. */
|
||||
export function declareArgumentCount(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
prologue: IrStmt[],
|
||||
): void {
|
||||
const slot = argumentCountSlot(lowerer, decl);
|
||||
if (slot === null) return;
|
||||
const loc = locOf(decl);
|
||||
const local = lowerer.declareHiddenLocal("%argc", F64);
|
||||
const stored = varRef(slot, F64, loc);
|
||||
prologue.push(
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: local.id,
|
||||
init: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: stored,
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "numLit", value: declaredCount(decl), type: F64, loc },
|
||||
else_: stored,
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "assign", localId: slot, value: { kind: "numLit", value: 0, type: F64, loc }, loc },
|
||||
);
|
||||
lowerer.ctx.argumentCount = local;
|
||||
}
|
||||
|
||||
/** `decl` as a value at `reference`: the plain closure when every call
|
||||
* through the expected function type passes the declared count, otherwise
|
||||
* an adapter that stores the expected type's count in the slot first. */
|
||||
export function argumentCountValue(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
reference: ts.Expression,
|
||||
closure: IrExpr & { type: IrType & { kind: "func" } },
|
||||
): IrExpr {
|
||||
const slot = argumentCountSlot(lowerer, decl);
|
||||
if (slot === null) return closure;
|
||||
const count = contextualCallCount(lowerer, reference);
|
||||
if (count === null || count === "varies") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
reference,
|
||||
count === null
|
||||
? "functions that read 'arguments.length' used as values where no function type is expected (the count each call passes is not known)"
|
||||
: "functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call)",
|
||||
);
|
||||
}
|
||||
const declared = declaredCount(decl);
|
||||
if (count <= declared) return closure;
|
||||
const loc = locOf(reference);
|
||||
const type = closure.type;
|
||||
const key = `argc:${slot}:${count}:${typeKey(type)}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = keyedHelperName(lowerer, "%argc.adapter", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const params = type.params.map((t, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: t }));
|
||||
lowerer.liftedFns.push(
|
||||
...buildFunctionAdapter(
|
||||
name,
|
||||
type,
|
||||
type,
|
||||
params,
|
||||
[],
|
||||
[
|
||||
{
|
||||
kind: "assign",
|
||||
localId: slot,
|
||||
value: { kind: "numLit", value: count, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", type, loc),
|
||||
args: params.map((p) => varRef(p.localId, p.type, loc)),
|
||||
type: type.ret,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
),
|
||||
);
|
||||
}
|
||||
return { kind: "call", callee: name, args: [closure], type, loc };
|
||||
}
|
||||
|
||||
/** The argument count every call through the function type `node` is
|
||||
* expected as passes: that type's parameter count when it has one call
|
||||
* signature with a fixed parameter list (a `this` parameter is type-only),
|
||||
* "varies" when it has optional or rest parameters, null when there is no
|
||||
* such type. */
|
||||
export function contextualCallCount(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
): number | "varies" | null {
|
||||
const contextual = lowerer.checker.getContextualType(node);
|
||||
if (contextual === undefined) return null;
|
||||
const signatures = lowerer.checker.getCallSignatures(
|
||||
lowerer.checker.getNonNullableType(contextual),
|
||||
);
|
||||
if (signatures.length !== 1) return null;
|
||||
const declaration = signatures[0]!.declaration;
|
||||
if (declaration === undefined) return null;
|
||||
let count = 0;
|
||||
for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) {
|
||||
if (isThisParameter(parameter)) continue;
|
||||
if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken)
|
||||
return "varies";
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Whether a program reference to the function declaration or method `fn`
|
||||
* reads it as a value (`xs.map(f)`, `const g = f`, `xs.map(C.parse)`) rather
|
||||
* than calling it. Export specifiers and default exports only name a
|
||||
* function; the importing files' references resolve to it and are checked
|
||||
* the same way. */
|
||||
export function usedAsValue(
|
||||
lowerer: Lowerer,
|
||||
fn: ts.FunctionDeclaration | ts.MethodDeclaration,
|
||||
): boolean {
|
||||
const name = fn.name;
|
||||
if (!name || !(ts.isIdentifier(name) || ts.isPrivateIdentifier(name))) return false;
|
||||
const method = ts.isMethodDeclaration(fn);
|
||||
const symbolAt = (n: ts.Node): ts.Symbol | null | undefined =>
|
||||
method
|
||||
? lowerer.checker.getSymbolAtLocation(n)
|
||||
: lowerer.resolveValueSymbol(n as ts.Identifier);
|
||||
const symbol = symbolAt(name);
|
||||
if (!symbol) return false;
|
||||
let found = false;
|
||||
for (const sf of lowerer.program.getSourceFiles()) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (found) return "stop";
|
||||
if (ts.isTypeNode(n)) return "skip";
|
||||
if (n === name || !(ts.isIdentifier(n) || ts.isPrivateIdentifier(n))) return undefined;
|
||||
if (n.text !== name.text || !n.parent) return undefined;
|
||||
const parent = n.parent;
|
||||
// A method is read through its member name (`C.parse`, `this.#m`).
|
||||
const read = ts.isPropertyAccessExpression(parent) && parent.name === n ? parent : n;
|
||||
if (method && read === n && !ts.isBindingElement(parent)) return undefined;
|
||||
const use = read.parent;
|
||||
if (use && ts.isCallExpression(use) && use.expression === read) return undefined;
|
||||
if (ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) return undefined;
|
||||
if (ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return undefined;
|
||||
if (symbolAt(n) === symbol) found = true;
|
||||
return undefined;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function fixedArity(fn: ts.FunctionLikeDeclaration): boolean {
|
||||
return fn.parameters.nodes.every(
|
||||
(p) =>
|
||||
p.questionToken === undefined &&
|
||||
p.initializer === undefined &&
|
||||
p.dotDotDotToken === undefined,
|
||||
);
|
||||
}
|
||||
|
||||
function declaredCount(fn: ts.FunctionLikeDeclaration): number {
|
||||
return fn.parameters.nodes.filter((p) => !isThisParameter(p)).length;
|
||||
}
|
||||
@@ -19,6 +19,18 @@ export function bindingNeverReassigned(
|
||||
): boolean {
|
||||
const cached = lowerer.neverReassignedCache.get(sym);
|
||||
if (cached !== undefined) return cached;
|
||||
const written = fileWritesBinding(sym, ts.getSourceFileOfNode(decl), resolve);
|
||||
lowerer.neverReassignedCache.set(sym, !written);
|
||||
return !written;
|
||||
}
|
||||
|
||||
/** True when something in `file` writes `sym` (the writes
|
||||
* bindingNeverReassigned counts), resolving identifiers with `resolve`. */
|
||||
export function fileWritesBinding(
|
||||
sym: ts.Symbol,
|
||||
file: ts.SourceFile,
|
||||
resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined,
|
||||
): boolean {
|
||||
let written = false;
|
||||
// Text pre-check keeps the file walk cheap: only same-named
|
||||
// identifiers pay a symbol resolution.
|
||||
@@ -62,9 +74,8 @@ export function bindingNeverReassigned(
|
||||
}
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
|
||||
lowerer.neverReassignedCache.set(sym, !written);
|
||||
return !written;
|
||||
ts.forEachChild(file, visit);
|
||||
return written;
|
||||
}
|
||||
|
||||
/** Strips the value-preserving wrappers off an expression: parens,
|
||||
|
||||
@@ -2,7 +2,13 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js";
|
||||
import {
|
||||
DATE_METHOD_HINT,
|
||||
dateJsonType,
|
||||
dateSetterTime,
|
||||
dateValueMethod,
|
||||
isDateSetter,
|
||||
} from "./dates.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -248,6 +254,22 @@ export function lowerInheritedDateMethodCall(
|
||||
type: result.type,
|
||||
loc,
|
||||
});
|
||||
if (isDateSetter(name) && name !== "setTime") {
|
||||
// The field setters change the Date the class's time slot holds once
|
||||
// the arguments are evaluated: an argument that calls setTime on the
|
||||
// instance gives the slot a new Date.
|
||||
const date = lowerer.declareHiddenLocal("%dateTime", DATE_T);
|
||||
const current = varRef(date.id, DATE_T, loc);
|
||||
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc };
|
||||
const next = dateSetterTime(lowerer, call, name, stmts, time);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: date.id,
|
||||
init: timeOf(lowerer, value, root, loc),
|
||||
loc,
|
||||
});
|
||||
return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc });
|
||||
}
|
||||
if (name === "setTime") {
|
||||
// Every argument is evaluated before setTime converts the first one.
|
||||
let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc };
|
||||
|
||||
@@ -2,12 +2,15 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerToNumberArguments } from "../lower-exprs.js";
|
||||
import { programDeclaresDateClass } from "./date-classes.js";
|
||||
import {
|
||||
BOOL,
|
||||
DATE_T,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
NULL_T,
|
||||
@@ -36,6 +39,32 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
|
||||
getTimezoneOffset: "date.getTimezoneOffset",
|
||||
};
|
||||
|
||||
/** The setters that change calendar fields: the field code
|
||||
* scr_date_set_fields takes (scr_number.h SCR_DATE_SET_*; 8 marks the UTC
|
||||
* forms) and the most arguments each takes. */
|
||||
const DATE_FIELD_SETTERS: ReadonlyMap<string, { code: number; max: number }> = new Map([
|
||||
["setMilliseconds", { code: 0, max: 1 }],
|
||||
["setSeconds", { code: 1, max: 2 }],
|
||||
["setMinutes", { code: 2, max: 3 }],
|
||||
["setHours", { code: 3, max: 4 }],
|
||||
["setDate", { code: 4, max: 1 }],
|
||||
["setMonth", { code: 5, max: 2 }],
|
||||
["setFullYear", { code: 6, max: 3 }],
|
||||
["setUTCMilliseconds", { code: 8, max: 1 }],
|
||||
["setUTCSeconds", { code: 9, max: 2 }],
|
||||
["setUTCMinutes", { code: 10, max: 3 }],
|
||||
["setUTCHours", { code: 11, max: 4 }],
|
||||
["setUTCDate", { code: 12, max: 1 }],
|
||||
["setUTCMonth", { code: 13, max: 2 }],
|
||||
["setUTCFullYear", { code: 14, max: 3 }],
|
||||
]);
|
||||
|
||||
/** Whether `name` is a Date setter scriptc lowers: setTime or a field
|
||||
* setter. */
|
||||
export function isDateSetter(name: string): boolean {
|
||||
return name === "setTime" || DATE_FIELD_SETTERS.has(name);
|
||||
}
|
||||
|
||||
const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
["toString", "date.toString"],
|
||||
["toDateString", "date.toDateString"],
|
||||
@@ -43,11 +72,11 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
|
||||
]);
|
||||
|
||||
export const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering";
|
||||
|
||||
/** The Date slice: statics plus read-only Date values backed by one
|
||||
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
|
||||
* identity and mutating methods remain fenced. */
|
||||
/** Date statics and methods on Date objects (a TimeClip'd millisecond
|
||||
* time value with identity): getters, text forms and setters.
|
||||
* Construction lives in lowerNew. */
|
||||
export function lowerDateCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -67,16 +96,18 @@ export function lowerDateCall(
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
// A Date a JavaScript file constructs is a mutable object; setTime on
|
||||
// it takes the dynamic method call. So does every Date method on a
|
||||
// A Date a JavaScript file constructs is a checked-dynamic value; a
|
||||
// setter on it takes the dynamic method call, which changes the Date the
|
||||
// handle holds. So does every Date method on a
|
||||
// checked-dynamic receiver when the program declares a class that extends
|
||||
// Date: the value can be an instance, whose class may declare the method.
|
||||
if (
|
||||
(name === "setTime" || programDeclaresDateClass(lowerer)) &&
|
||||
(isDateSetter(name) || programDeclaresDateClass(lowerer)) &&
|
||||
tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn"
|
||||
)
|
||||
return null;
|
||||
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
|
||||
if (isDateSetter(name)) return lowerDateSetter(lowerer, call, access, name);
|
||||
if (call.arguments.nodes.length !== 0) {
|
||||
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
|
||||
}
|
||||
@@ -253,3 +284,151 @@ export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
lowerer.isStdlibMember(access)
|
||||
);
|
||||
}
|
||||
|
||||
/** `d.setTime(t)` and the field setters (`d.setHours(h, m?, s?, ms?)` ...)
|
||||
* on a Date object: the receiver, then the arguments in order, then the
|
||||
* new time value (scr_date_set_fields over the time value the Date holds
|
||||
* once the arguments have run) stored into the Date and answered. Missing
|
||||
* optional arguments keep their fields; arguments past a setter's last
|
||||
* one are evaluated and ignored. A Date local that holds its time value
|
||||
* (date-values.ts) takes the new time value itself. */
|
||||
function lowerDateSetter(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
name: string,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const nodes = call.arguments.nodes;
|
||||
if (nodes.some((node) => ts.isSpreadElement(node)))
|
||||
lowerer.noLowering(`Date.prototype.${name} with spread arguments`, call, DATE_METHOD_HINT);
|
||||
const timeLocal = timeValueLocalOf(lowerer, access.expression);
|
||||
const stmts: IrStmt[] = [];
|
||||
let receiver: IrExpr | null = null;
|
||||
if (timeLocal === null) {
|
||||
// A checked-dynamic receiver takes the dynamic method call
|
||||
// (lowerDateCall), so the value here is a Date object.
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
if (value.type.kind !== "date")
|
||||
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
|
||||
const date = lowerer.declareHiddenLocal("%date", DATE_T);
|
||||
stmts.push({ kind: "varDecl", localId: date.id, init: value, loc });
|
||||
receiver = { kind: "varRef", localId: date.id, type: DATE_T, loc };
|
||||
}
|
||||
const time: IrExpr =
|
||||
receiver === null
|
||||
? { kind: "varRef", localId: timeLocal!.id, type: F64, loc }
|
||||
: { kind: "libCall", fn: "date.getTime", args: [receiver], type: F64, loc };
|
||||
const next = dateSetterTime(lowerer, call, name, stmts, time);
|
||||
if (receiver === null) {
|
||||
stmts.push({ kind: "assign", localId: timeLocal!.id, value: next, loc });
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "varRef", localId: timeLocal!.id, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "libCall", fn: "date.store", args: [receiver, next], type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The new time value of setter `name` called with `call`'s arguments on a
|
||||
* Date whose time value `time` reads: the arguments evaluate into
|
||||
* temporaries appended to `stmts` (ToNumber of each, an undefined one is
|
||||
* NaN), and the result reads `time` after them. A missing first argument
|
||||
* is NaN; missing optional arguments keep their fields; arguments past a
|
||||
* setter's last one are evaluated and ignored. */
|
||||
export function dateSetterTime(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
stmts: IrStmt[],
|
||||
time: IrExpr,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const converted = lowerToNumberArguments(
|
||||
lowerer,
|
||||
call.arguments.nodes as readonly ts.Expression[],
|
||||
);
|
||||
stmts.push(...converted.stmts);
|
||||
const args: IrExpr[] = converted.values.map((value) => {
|
||||
if (value.kind === "numLit") return value;
|
||||
const arg = lowerer.declareHiddenLocal("%dateArg", F64);
|
||||
stmts.push({ kind: "varDecl", localId: arg.id, init: value, loc });
|
||||
return { kind: "varRef", localId: arg.id, type: F64, loc };
|
||||
});
|
||||
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
if (name === "setTime")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.timeClip",
|
||||
args: [args[0] ?? number(Number.NaN)],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const setter = DATE_FIELD_SETTERS.get(name)!;
|
||||
// The first argument is required: a missing one is undefined, so NaN.
|
||||
const used = args.length === 0 ? [number(Number.NaN)] : args.slice(0, setter.max);
|
||||
const operands = [...used];
|
||||
while (operands.length < 4) operands.push(number(0));
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.setFields",
|
||||
args: [time, number(setter.code), number(used.length), ...operands],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The local a Date binding that holds its time value lowers to, for an
|
||||
* identifier `node` that names one (date-values.ts); null otherwise. */
|
||||
function timeValueLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | null {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (!ts.isIdentifier(node)) return null;
|
||||
const local = lowerer.resolveLocal(node);
|
||||
return local !== undefined && local !== null && lowerer.timeValueDateLocals.has(local)
|
||||
? local
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `d[Symbol.toPrimitive](hint)` on a Date with a literal hint: "number"
|
||||
* answers the time value, "string" and "default" the toString() text (a
|
||||
* Date's default hint is a string). Null for other calls. */
|
||||
export function lowerDateToPrimitiveCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.ElementAccessExpression,
|
||||
): IrExpr | null {
|
||||
const key = access.argumentExpression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
key.name.text !== "toPrimitive" ||
|
||||
!ts.isIdentifier(key.expression) ||
|
||||
!lowerer.isStdlibGlobal(key.expression, "Symbol") ||
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date"
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
const hint = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null;
|
||||
const text = hint !== null && ts.isStringLiteralLike(hint) ? hint.text : null;
|
||||
if (text !== "number" && text !== "string" && text !== "default")
|
||||
return lowerer.noLowering(
|
||||
'Date.prototype[Symbol.toPrimitive] without a "number", "string" or "default" literal hint',
|
||||
call,
|
||||
);
|
||||
const raw = lowerer.lowerExpr(access.expression);
|
||||
const date: IrExpr =
|
||||
raw.type.kind === "dyn"
|
||||
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: DATE_T, loc }
|
||||
: raw;
|
||||
return text === "number"
|
||||
? { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc }
|
||||
: { kind: "libCall", fn: "date.toString", args: [date], type: STRING, loc };
|
||||
}
|
||||
|
||||
@@ -185,6 +185,32 @@ export function builtinMemberRequireDecl(
|
||||
return spec !== null && canonicalBuiltinModule(spec) !== null;
|
||||
}
|
||||
|
||||
/** The member's own symbol for a variable bound to a builtin module's
|
||||
* member (`const SD = require("string_decoder").StringDecoder`,
|
||||
* builtinMemberRequireDecl) when the member is a class, so the variable
|
||||
* resolves as a named import of the class does. The symbol itself
|
||||
* otherwise. */
|
||||
export function builtinMemberBindingTarget(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol | null,
|
||||
): ts.Symbol | null {
|
||||
const decl = symbol?.valueDeclaration;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!builtinMemberRequireDecl(decl.name, decl.initializer)
|
||||
)
|
||||
return symbol;
|
||||
const init = decl.initializer as ts.PropertyAccessExpression;
|
||||
let member = lowerer.checker.getSymbolAtLocation(init.name);
|
||||
if (member && member.flags & ts.SymbolFlags.Alias)
|
||||
member = lowerer.checker.getAliasedSymbol(member);
|
||||
// Only a class member: `require("events").EventEmitter` is a static
|
||||
// property of the exported class, which builtinEmitterInfoOf follows
|
||||
// from the variable itself.
|
||||
return member?.declarations.some(ts.isClassDeclaration) ? member : symbol;
|
||||
}
|
||||
|
||||
/** The supported builtin module when `decl` destructures a builtin
|
||||
* NAMESPACE binding (`const crypto = require('crypto'); const {
|
||||
* createSign, sign: mySign } = crypto;` — test/common/crypto.js's
|
||||
|
||||
@@ -2,7 +2,7 @@ import { nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import { canonicalBuiltinModule, isNodeEsmFile, locOf } from "../../program.js";
|
||||
import { canonicalBuiltinModule, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js";
|
||||
import { isRelativeSpecifier } from "../../workspace-registry.js";
|
||||
import { probeNodeRequireRefusal } from "../../npm.js";
|
||||
import { isNpmStaticPackage } from "../../npm-static.js";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
invalidJsonModuleDiag,
|
||||
nativeAddonDiag,
|
||||
noLoweringDiag,
|
||||
unsupportedPackageImportDiag,
|
||||
} from "../../../diagnostics/diagnostic.js";
|
||||
import { NODE_BUILTIN_MODULES_V24 } from "../surfaces.js";
|
||||
@@ -24,12 +25,14 @@ import {
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
NULL_T,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
funcOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerFfiMemoryModule } from "../native-ffi.js";
|
||||
import {
|
||||
@@ -38,6 +41,7 @@ import {
|
||||
createRequireCalleeFileOf,
|
||||
} from "./module-bindings.js";
|
||||
import { stripTypeCasts } from "./arguments.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
|
||||
/** node:module's two compiler-only calls. isBuiltin compares one evaluated
|
||||
* string against the pinned Node 24 list (bare builtins also accept their
|
||||
@@ -134,6 +138,94 @@ export function lowerNodeModuleCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** The fence a createRequire use with no build-time lowering throws in
|
||||
* JavaScript source, where preflight admits it inside a try block
|
||||
* (createRequireUse7 in program.ts). The diagnostic joins the runtime-fence
|
||||
* ledger, so coverage lists the use as deferred to runtime. */
|
||||
function createRequireFence(
|
||||
lowerer: Lowerer,
|
||||
surface: string,
|
||||
node: ts.Node,
|
||||
hint: string,
|
||||
): IrStmt & { kind: "runtimeFence" } {
|
||||
const loc = locOf(node);
|
||||
const d = noLoweringDiag(surface, loc, hint);
|
||||
lowerer.runtimeFences.push(d);
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
return {
|
||||
kind: "runtimeFence",
|
||||
code: d.code,
|
||||
message: `${d.message} [${d.code} at ${loc.file}:${line}]`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A createRequire loader's `resolve` (or `resolve.paths`) read as a value
|
||||
* in JavaScript source: a function that throws the fence when called, so
|
||||
* `(options.resolve ?? require.resolve)(id)` still calls the option when
|
||||
* one is given. */
|
||||
export function createRequireResolveFenceValue(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const surface = "createRequire's require.resolve as a value";
|
||||
const hint =
|
||||
'call it with a string literal (require.resolve("<specifier>")), which resolves at build time';
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(access))) lowerer.noLowering(surface, access, hint);
|
||||
const name = keyedHelperName(lowerer, "%createRequire.fence", `${loc.file}:${loc.start}`);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [createRequireFence(lowerer, surface, access, hint)],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
|
||||
fnName: access.name.text,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A createRequire use deferred to run time in JavaScript source: as in
|
||||
* Node, the call's arguments are evaluated in order, then the call throws
|
||||
* (here, the fence). Code before the call in the same statement runs, and
|
||||
* the expression diverges, so nothing after it does. */
|
||||
function deferredCreateRequireThrow(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
surface: string,
|
||||
hint: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({
|
||||
kind: "exprStmt",
|
||||
expr: lowerer.lowerExpr(arg),
|
||||
loc: locOf(arg),
|
||||
}));
|
||||
const type = lowerer.irTypeOf(call);
|
||||
const slot = lowerer.declareHiddenLocal("%never", type);
|
||||
slot.mutable = true;
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: slot.id, init: null, loc },
|
||||
createRequireFence(lowerer, surface, call, hint),
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: varRef(slot.id, type, loc),
|
||||
diverges: true,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerCreateRequireCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -142,11 +234,11 @@ export function lowerCreateRequireCall(
|
||||
const cr = createRequireSpecOf(lowerer, call);
|
||||
if (cr === null) return null;
|
||||
if (cr.spec === null) {
|
||||
lowerer.noLowering(
|
||||
"createRequire's require with this argument shape",
|
||||
call,
|
||||
'the compiled module graph is fixed at build time — the one lowered form is require("<static string literal>")',
|
||||
);
|
||||
const surface = "createRequire's require with a computed specifier or this argument shape";
|
||||
const hint =
|
||||
'the compiled module graph is fixed at build time — the one lowered form is require("<static string literal>"); native addons are not loaded';
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(call))) lowerer.noLowering(surface, call, hint);
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
|
||||
}
|
||||
const spec = cr.spec;
|
||||
if (spec === "node:ffi") {
|
||||
@@ -367,11 +459,30 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
|
||||
}
|
||||
const baseFile = requireResolverBaseFile(lowerer, receiver);
|
||||
if (baseFile === null) return null;
|
||||
const surface = pathsCall ? "require.resolve.paths" : "require.resolve";
|
||||
// A createRequire loader's query that resolves only at run time, in
|
||||
// JavaScript source: preflight admits it inside a try block
|
||||
// (createRequireUse7 in program.ts). Its arguments run, then it throws.
|
||||
const deferred = (what: string, hint: string): IrExpr | null =>
|
||||
call.questionDotToken === undefined &&
|
||||
createRequireCalleeFileOf(lowerer, receiver) !== null &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call))
|
||||
? deferredCreateRequireThrow(
|
||||
lowerer,
|
||||
call,
|
||||
`createRequire's ${surface} ${what}`,
|
||||
hint,
|
||||
locOf(call),
|
||||
)
|
||||
: null;
|
||||
const resolveHint = `call it with a string literal (${surface}("<specifier>")), which resolves at build time`;
|
||||
if (
|
||||
call.questionDotToken !== undefined ||
|
||||
call.arguments.nodes.length < 1 ||
|
||||
call.arguments.nodes.length > (pathsCall ? 1 : 2)
|
||||
) {
|
||||
const fenced = deferred("with this argument shape", resolveHint);
|
||||
if (fenced) return fenced;
|
||||
lowerer.noLowering(
|
||||
pathsCall
|
||||
? "require.resolve.paths with this argument shape"
|
||||
@@ -381,6 +492,8 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
|
||||
}
|
||||
const specifier = staticString(call.arguments.nodes[0]);
|
||||
if (specifier === null) {
|
||||
const fenced = deferred("with a computed request", resolveHint);
|
||||
if (fenced) return fenced;
|
||||
lowerer.noLowering(
|
||||
`${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`,
|
||||
call.arguments.nodes[0]!,
|
||||
@@ -416,6 +529,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
|
||||
}
|
||||
const paths = staticResolveOptionPaths(call.arguments.nodes[1]);
|
||||
if (paths === null) {
|
||||
const fenced = deferred(
|
||||
"with runtime-computed options",
|
||||
'omit options or pass { paths: ["<static directory>", ...] }',
|
||||
);
|
||||
if (fenced) return fenced;
|
||||
lowerer.noLowering(
|
||||
"require.resolve with runtime-computed options",
|
||||
call.arguments.nodes[1]!,
|
||||
|
||||
@@ -302,6 +302,15 @@ function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | n
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `new readline.Interface(...)` and `new readlinePromises.Interface(...)`:
|
||||
* the constructor createInterface calls, with the same arguments. Null for
|
||||
* other constructors. */
|
||||
export function lowerReadlineInterfaceNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
const module = interfaceClassModule(lowerer, expr.expression);
|
||||
if (module === null) return null;
|
||||
return lowerReadlineCreateCall(lowerer, expr, locOf(expr), module === "readline/promises");
|
||||
}
|
||||
|
||||
/** `rl instanceof readline.Interface` and `rl instanceof
|
||||
* readlinePromises.Interface` over a readline Interface: an interface is
|
||||
* an instance of the class of the module that created it (Node's
|
||||
@@ -1059,10 +1068,12 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" |
|
||||
* goes through rl.terminal with the completer. */
|
||||
export function lowerReadlineCreateCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
expr: ts.CallExpression | ts.NewExpression,
|
||||
loc: SrcLoc,
|
||||
promises: boolean,
|
||||
): IrExpr {
|
||||
const argNodes = expr.arguments?.nodes ?? [];
|
||||
const what = ts.isNewExpression(expr) ? "new Interface" : "createInterface";
|
||||
let input: IrExpr | null = null;
|
||||
let output: IrExpr | "stdout" | "none" = "none";
|
||||
let crlf: IrExpr = numLit(0, loc);
|
||||
@@ -1112,39 +1123,38 @@ export function lowerReadlineCreateCall(
|
||||
});
|
||||
terminal = { kind: "varRef", localId: local.id, type: BOOL, loc };
|
||||
};
|
||||
const optsNode = expr.arguments.nodes[0];
|
||||
const optsNode = argNodes[0];
|
||||
if (optsNode === undefined) {
|
||||
lowerer.noLowering(
|
||||
"createInterface without arguments",
|
||||
`${what} without arguments`,
|
||||
expr,
|
||||
"pass { input: process.stdin, output: process.stdout } or createInterface(input, output)",
|
||||
);
|
||||
}
|
||||
if (!ts.isObjectLiteralExpression(optsNode)) {
|
||||
// createInterface(input, output?, completer?, terminal?)
|
||||
if (expr.arguments.nodes.length > 4) {
|
||||
if (argNodes.length > 4) {
|
||||
lowerer.noLowering(
|
||||
`createInterface with ${expr.arguments.nodes.length} arguments`,
|
||||
expr.arguments.nodes[4]!,
|
||||
`${what} with ${argNodes.length} arguments`,
|
||||
argNodes[4]!,
|
||||
"pass createInterface(input, output, completer, terminal) or one options object",
|
||||
);
|
||||
}
|
||||
input = lowerInput(lowerer, optsNode);
|
||||
if (expr.arguments.nodes[1] !== undefined)
|
||||
output = lowerOutput(lowerer, expr.arguments.nodes[1]);
|
||||
const completerNode = expr.arguments.nodes[2];
|
||||
if (argNodes[1] !== undefined) output = lowerOutput(lowerer, argNodes[1]);
|
||||
const completerNode = argNodes[2];
|
||||
if (
|
||||
completerNode !== undefined &&
|
||||
!(ts.isIdentifier(completerNode) && completerNode.text === "undefined")
|
||||
) {
|
||||
setCompleter(completerNode);
|
||||
}
|
||||
if (expr.arguments.nodes[3] !== undefined) setTerminal(expr.arguments.nodes[3]);
|
||||
if (argNodes[3] !== undefined) setTerminal(argNodes[3]);
|
||||
} else {
|
||||
if (expr.arguments.nodes.length > 1) {
|
||||
if (argNodes.length > 1) {
|
||||
lowerer.noLowering(
|
||||
"createInterface with more than one argument",
|
||||
expr.arguments.nodes[1]!,
|
||||
`${what} with more than one argument`,
|
||||
argNodes[1]!,
|
||||
"pass every option in the one options object",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { builtinModuleFnOf } from "../surfaces.js";
|
||||
import { BOOL, DYN, type IrExpr, STRING, arrayOf } from "../../../ir/ir.js";
|
||||
import { hasRuntimeStatics } from "../class-runtime-statics.js";
|
||||
import { BOOL, DYN, type IrExpr, STRING, arrayOf, isUnitType } from "../../../ir/ir.js";
|
||||
|
||||
/** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target,
|
||||
* thisArg, argsList) where the TARGET is a builtin
|
||||
@@ -63,6 +64,37 @@ export function lowerReflectCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Reflect.deleteProperty over a value with a live property table: a
|
||||
// checked-dynamic value, or a JavaScript class whose statics are runtime
|
||||
// properties of its constructor
|
||||
// (`Reflect.deleteProperty(Response, "getResponseState")`).
|
||||
if (
|
||||
reflectMember === "deleteProperty" &&
|
||||
call.arguments.nodes.length === 2 &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
let target = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const info =
|
||||
target.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined;
|
||||
// A primitive target throws the TypeError, so it boxes as is.
|
||||
const primitive =
|
||||
isUnitType(target.type) ||
|
||||
["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind);
|
||||
if (
|
||||
primitive ||
|
||||
(info !== undefined && hasRuntimeStatics(info) && lowerer.dynConvertible(target.type))
|
||||
)
|
||||
target = lowerer.coerceToExpected(target, DYN);
|
||||
if (target.type.kind !== "dyn") return null;
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectDeleteProperty",
|
||||
args: [target, lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (reflectMember === "get" || reflectMember === "set") {
|
||||
const required = reflectMember === "get" ? 2 : 3;
|
||||
if (
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
import {
|
||||
classPrototypeData,
|
||||
hasClassPrototypeData,
|
||||
prototypeReflectedFlag,
|
||||
observedPrototypeMethod,
|
||||
reflectClassPrototype,
|
||||
} from "./class-prototypes.js";
|
||||
@@ -262,6 +263,7 @@ const KEY_CALLS = new Map<string, "object" | "any" | "value">([
|
||||
["dyn.propertyIsEnumerableComputed", "any"],
|
||||
["dyn.hasOwnComputed", "value"],
|
||||
["dyn.keyDeleteComputed", "value"],
|
||||
["dyn.reflectDeleteProperty", "object"],
|
||||
["dyn.keySetComputed", "value"],
|
||||
]);
|
||||
|
||||
@@ -4179,6 +4181,103 @@ export class ClassDynamicDispatch {
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** `%dyn.class.bagHasOwn`: whether an instance's property bag holds `key`
|
||||
* as its own property. False before the instance has a bag. */
|
||||
private readBagHasOwn(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const name = "%dyn.class.bagHasOwn";
|
||||
let helper = lowerer.liftedFnNamed(name);
|
||||
if (!helper) {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
helper = {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "p.0", name: "bag", type: DYN },
|
||||
{ localId: "p.1", name: "key", type: STRING },
|
||||
],
|
||||
returnType: BOOL,
|
||||
locals: [
|
||||
{ id: "p.0", name: "bag", type: DYN, mutable: false },
|
||||
{ id: "p.1", name: "key", type: STRING, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
|
||||
then: [
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.hasOwn",
|
||||
args: [value, varRef("p.1", STRING, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push(helper);
|
||||
}
|
||||
this.generated.add(helper);
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** The `constructor` an instance of `info` inherits when it has no own
|
||||
* one. The class's prototype view answers its own `constructor` (a
|
||||
* replacement, or the class once reflection installed it). A view
|
||||
* without one answers the class itself unless reflection had populated it
|
||||
* (prototypeReflectedFlag, set when the view initializes): then the
|
||||
* program deleted the property, and the read continues to the base
|
||||
* class's prototype and past the last one to Object.prototype, as in
|
||||
* Node. A prototype's own `constructor` is read with the instance
|
||||
* `receiver` as `this`, so an accessor's getter runs on the instance. */
|
||||
private inheritedConstructor(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
key: IrExpr,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN);
|
||||
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null;
|
||||
if (!prototype) return cls;
|
||||
const base = info.base;
|
||||
const read = (): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectGet",
|
||||
args: [prototype, lowerer.coerceToExpected(key, DYN), receiver],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
// Past the last class prototype, the read continues to Object.prototype's data property.
|
||||
const next: IrExpr =
|
||||
base && !base.builtinError && base.decl && !base.generic && !base.localClass
|
||||
? this.inheritedConstructor(lowerer, base, key, receiver, loc)
|
||||
: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc };
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc },
|
||||
then: read(),
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc),
|
||||
then: next,
|
||||
else_: cls,
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The hints for which some boxed class declares a conversion method. A
|
||||
* string conversion calls toString first, so without a declared toString
|
||||
* or Symbol.toPrimitive it answers Object.prototype.toString's
|
||||
@@ -5937,15 +6036,19 @@ export class ClassDynamicDispatch {
|
||||
!info.classDecorators
|
||||
) {
|
||||
this.constructors.set(info.def.name, info);
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
// `constructor` is an own property of every class prototype, so an
|
||||
// ancestor's prototype answers for an instance of this class only
|
||||
// after the program deletes the class's own: the instance's own
|
||||
// property answers, then the class's prototype (inheritedConstructor).
|
||||
const answered = this.readBagHasOwn(lowerer, bag, key, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
cond: answered,
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
@@ -5953,7 +6056,13 @@ export class ClassDynamicDispatch {
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: cls,
|
||||
else_: this.inheritedConstructor(
|
||||
lowerer,
|
||||
info,
|
||||
key,
|
||||
lowerer.coerceToExpected(receiver, DYN),
|
||||
loc,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -378,8 +378,33 @@ function reflectedMethodValue(
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
||||
info.localClass !== undefined ||
|
||||
lowerer.errorHierarchyClassOf(info.def.name);
|
||||
// `super.m(...)` in a prototype method reads the base class's prototype,
|
||||
// which lowerSuperMethodCall answers over the call-site receiver when the
|
||||
// base has a prototype object. Other uses of `super` keep the native
|
||||
// receiver.
|
||||
const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base);
|
||||
// Over a dynamic receiver, private fields are brand-checked and read, and private methods
|
||||
// brand-checked and called; a private accessor, or a private method read as a value, would become a
|
||||
// lookup of its name.
|
||||
const privateAccessors = new Set<string>();
|
||||
const privateMethods = new Set<string>();
|
||||
for (const other of declaration.members.nodes)
|
||||
if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) {
|
||||
if (ts.isAccessor(other)) privateAccessors.add(other.name.text);
|
||||
else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text);
|
||||
}
|
||||
ts.walkPreorder(member, (node) => {
|
||||
if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword)
|
||||
if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node)))
|
||||
needsNativeReceiver = true;
|
||||
if (!ts.isPrivateIdentifier(node)) return;
|
||||
// Private members on a static method's receiver (the class value) keep
|
||||
// the native receiver; prototype methods brand-check theirs, except
|
||||
// private accessors and private methods used other than by a call.
|
||||
if (
|
||||
staticMethod ||
|
||||
privateAccessors.has(node.text) ||
|
||||
(privateMethods.has(node.text) && !isPrivateMethodCallee(node))
|
||||
)
|
||||
needsNativeReceiver = true;
|
||||
});
|
||||
if (needsNativeReceiver) return null;
|
||||
@@ -403,7 +428,11 @@ function reflectedMethodValue(
|
||||
loc: locOf(member),
|
||||
};
|
||||
}
|
||||
// The method may be lowered on top of an unrelated caller's frames (a
|
||||
// `super` call in another class's method reflects this class): its `this`
|
||||
// is its own receiver, never an enclosing function's.
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
context.thisBoundary = true;
|
||||
const previousClass = lowerer.currentClass;
|
||||
lowerer.currentClass = info;
|
||||
lowerer.fnStack.push(context);
|
||||
@@ -441,6 +470,55 @@ function reflectedMethodValue(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `base` and every class above it have a complete prototype object
|
||||
* (reflectClassPrototype): module-level program classes that neither extend
|
||||
* Date nor a runtime-provided class. */
|
||||
function prototypeChainViewable(base: ClassInfo): boolean {
|
||||
for (let c: ClassInfo | null = base; c; c = c.base)
|
||||
if (
|
||||
c.decl === null ||
|
||||
c.localClass !== undefined ||
|
||||
c.generic !== undefined ||
|
||||
c.genericInstance !== undefined ||
|
||||
c.mixinInstance !== undefined ||
|
||||
c.callableBase !== undefined ||
|
||||
c.extendsDate !== undefined ||
|
||||
c.def.runtime ||
|
||||
c.builtinEmitter ||
|
||||
c.builtinStream ||
|
||||
c.builtinError ||
|
||||
c.builtinEventTarget
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** `super` as the receiver of a called member: `super.m(...)`. */
|
||||
/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */
|
||||
function isPrivateMethodCallee(node: ts.Node): boolean {
|
||||
const access = node.parent;
|
||||
return (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.name === node &&
|
||||
access.parent !== undefined &&
|
||||
ts.isCallExpression(access.parent) &&
|
||||
access.parent.expression === access
|
||||
);
|
||||
}
|
||||
|
||||
function isSuperMethodCallee(node: ts.Node): boolean {
|
||||
const access = node.parent;
|
||||
return (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === node &&
|
||||
access.parent !== undefined &&
|
||||
ts.isCallExpression(access.parent) &&
|
||||
access.parent.expression === access
|
||||
);
|
||||
}
|
||||
|
||||
/** Complete adapters after all native class bodies and instantiations exist. */
|
||||
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import {
|
||||
classMemberNameOf,
|
||||
classValueRef,
|
||||
@@ -299,6 +299,23 @@ function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The global that is true once the class's prototype view has been
|
||||
* populated by reflection (reflectClassPrototype): the view then holds an
|
||||
* own `constructor` unless the program deleted it. A view that was never
|
||||
* reflected leaves `constructor` implicit. */
|
||||
export function prototypeReflectedFlag(lowerer: Lowerer, info: ClassInfo): string {
|
||||
if (info.prototypeReflectedFlag) return info.prototypeReflectedFlag;
|
||||
const id = `%g.%prototype.data.${info.def.name}.reflected`;
|
||||
lowerer.globalsList.push({
|
||||
id,
|
||||
name: `${info.def.jsName ?? info.def.name}.prototype.reflected`,
|
||||
type: BOOL,
|
||||
mutable: true,
|
||||
});
|
||||
info.prototypeReflectedFlag = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Populate the shared prototype view before exposing it to reflection.
|
||||
* Methods and accessors are non-enumerable and retain declaration identity. */
|
||||
export function reflectClassPrototype(
|
||||
@@ -399,8 +416,17 @@ export function reflectClassPrototype(
|
||||
{ key: "value", value: classValueRef(lowerer, info, info.decl) },
|
||||
writable,
|
||||
]),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: prototypeReflectedFlag(lowerer, info),
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
// The members lower on top of whatever frames requested the view (a
|
||||
// `super` call in a subclass's method); their `this` is their own receiver.
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
context.thisBoundary = true;
|
||||
lowerer.fnStack.push(context);
|
||||
try {
|
||||
for (const member of info.decl.members.nodes) {
|
||||
@@ -503,6 +529,69 @@ export function isErrorPrototypeValue(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a bare `C.prototype` read becomes the prototype of another object
|
||||
* or is only compared or inspected: the argument of `Object.create` or
|
||||
* `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument
|
||||
* of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of
|
||||
* `===` / `!==`. In these positions the complete view of the
|
||||
* prototype (reflectClassPrototype) answers. Objects that inherit from it
|
||||
* run the class's methods with themselves as the receiver, which the
|
||||
* methods' receiver-independent lowering (reflectedClassMethodValue)
|
||||
* supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`,
|
||||
* which would relink the class, keep the diagnostic. */
|
||||
function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
|
||||
const args = parent.arguments.nodes;
|
||||
const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object");
|
||||
const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect");
|
||||
if (
|
||||
objectMethod === "create" ||
|
||||
objectMethod === "getPrototypeOf" ||
|
||||
reflectMethod === "getPrototypeOf"
|
||||
)
|
||||
return args[0] === node;
|
||||
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
|
||||
return args[1] === node;
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
|
||||
);
|
||||
}
|
||||
|
||||
/** A module-level JavaScript class named directly (not a class value that
|
||||
* may hold a subclass at run time): its prototype has a complete view whose
|
||||
* methods accept any receiver. */
|
||||
function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean {
|
||||
return (
|
||||
info.decl !== null &&
|
||||
info.localClass === undefined &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
||||
exactClassOfReceiver(lowerer, receiver) === info
|
||||
);
|
||||
}
|
||||
|
||||
/** The complete prototype view of the class `node` names, when it is a
|
||||
* viewable class (util.inherits's superCtor); null otherwise. */
|
||||
export function classPrototypeView(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const info = exactClassOfReceiver(lowerer, node);
|
||||
if (!info || !viewableClass(lowerer, info, node)) return null;
|
||||
return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node));
|
||||
}
|
||||
|
||||
/** Named data and method slots have native descriptors. Bare reflection and
|
||||
* accessor replacement still require a complete view of the prototype. */
|
||||
export function lowerClassPrototypeData(
|
||||
@@ -566,6 +655,12 @@ export function lowerClassPrototypeData(
|
||||
});
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), receiver);
|
||||
}
|
||||
if (
|
||||
name === null &&
|
||||
prototypeViewPosition(lowerer, expr) &&
|
||||
viewableClass(lowerer, info, expr.expression)
|
||||
)
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr));
|
||||
if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isDateSetter } from "./builtins/dates.js";
|
||||
|
||||
/** Date methods whose receiver is only read for its time value: a Date
|
||||
* local used as their receiver can store the time value instead of a Date
|
||||
@@ -41,12 +42,15 @@ const RELATIONAL = new Set([
|
||||
|
||||
/** True when a local `const` (or a `let` that is never assigned again)
|
||||
* initialized with `new Date(...)` can keep the Date's time value instead
|
||||
* of a Date object: no use observes the Date's identity. Every use reads the time value as
|
||||
* a Date method receiver, a String() or template operand, a relational or
|
||||
* string `+` operand, or `new Date(d)`'s argument, inside the declaring
|
||||
* function, or inside the block or loop that scopes a module-level
|
||||
* binding (no closure captures it). Top-level module bindings keep the
|
||||
* object. Other uses box the time value into a new Date at the use. */
|
||||
* of a Date object: no use observes the Date's identity. Every use reads
|
||||
* the time value as a Date method receiver (a setter replaces it; the
|
||||
* arguments of a method that reads it call no setter on it), a
|
||||
* String(), Number() or template operand, a unary + or - operand, a
|
||||
* relational or string `+` operand, or `new Date(d)`'s argument, inside
|
||||
* the declaring function, or inside the block or loop that scopes a
|
||||
* module-level binding (no closure captures it). Top-level module bindings
|
||||
* keep the object. Other uses box the time value into a new Date at the
|
||||
* use. */
|
||||
export function isTimeValueDateDeclaration(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
@@ -94,17 +98,21 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return false;
|
||||
let ok = true;
|
||||
let sets = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (!ok) return;
|
||||
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
|
||||
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
|
||||
if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false;
|
||||
const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node, symbol) : null;
|
||||
if (use === null) ok = false;
|
||||
else if (use === "set") sets = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(scope);
|
||||
if (ok && sets) lowerer.timeValueDateWrites.add(decl);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -164,29 +172,75 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A use of the binding that reads only its time value. */
|
||||
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
/** How a use of the binding uses its time value: "read" for a use that
|
||||
* reads only the time value, "set" for a setter call that replaces it, or
|
||||
* null for any other use. */
|
||||
function timeValueUse(
|
||||
lowerer: Lowerer,
|
||||
id: ts.Identifier,
|
||||
symbol: ts.Symbol,
|
||||
): "read" | "set" | null {
|
||||
let node: ts.Node = id;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return null;
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
||||
const call = parent.parent;
|
||||
return (
|
||||
!parent.questionDotToken &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === parent &&
|
||||
!call.questionDotToken &&
|
||||
TIME_VALUE_METHODS.has(parent.name.text) &&
|
||||
lowerer.isStdlibMember(parent)
|
||||
);
|
||||
if (
|
||||
parent.questionDotToken ||
|
||||
!ts.isCallExpression(call) ||
|
||||
call.expression !== parent ||
|
||||
call.questionDotToken ||
|
||||
!lowerer.isStdlibMember(parent)
|
||||
)
|
||||
return null;
|
||||
if (isDateSetter(parent.name.text)) return "set";
|
||||
// The method reads the time value after its arguments are evaluated:
|
||||
// a setter there (`d.toJSON(d.setTime(0))`) changes what it reads.
|
||||
if (call.arguments.nodes.some((arg) => callsSetterOf(lowerer, arg, symbol))) return null;
|
||||
return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null;
|
||||
}
|
||||
return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null;
|
||||
}
|
||||
|
||||
/** True when `node` calls a setter on the binding `symbol` names. */
|
||||
function callsSetterOf(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
isDateSetter(node.expression.name.text)
|
||||
) {
|
||||
let receiver = node.expression.expression;
|
||||
while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression;
|
||||
if (ts.isIdentifier(receiver) && lowerer.checker.getSymbolAtLocation(receiver) === symbol)
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
ts.forEachChild(node, (child) => callsSetterOf(lowerer, child, symbol) || undefined) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/** A use of the binding, other than a method call, that reads only its
|
||||
* time value. */
|
||||
function readsTimeValue(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
parent: ts.Node,
|
||||
symbol: ts.Symbol,
|
||||
): boolean {
|
||||
if (ts.isTemplateSpan(parent)) return parent.expression === node;
|
||||
// Unary + and -, the number hint.
|
||||
if (ts.isPrefixUnaryExpression(parent))
|
||||
return (
|
||||
parent.operator === ts.SyntaxKind.PlusToken || parent.operator === ts.SyntaxKind.MinusToken
|
||||
);
|
||||
if (ts.isCallExpression(parent)) {
|
||||
return (
|
||||
parent.arguments.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "String")
|
||||
(lowerer.isStdlibGlobal(parent.expression, "String") ||
|
||||
lowerer.isStdlibGlobal(parent.expression, "Number"))
|
||||
);
|
||||
}
|
||||
if (ts.isNewExpression(parent)) {
|
||||
@@ -198,6 +252,9 @@ function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
// A left operand converts after the right one is evaluated: a setter
|
||||
// there changes the time value the comparison or concatenation reads.
|
||||
if (parent.left === node && callsSetterOf(lowerer, parent.right, symbol)) return false;
|
||||
const other = parent.left === node ? parent.right : parent.left;
|
||||
const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind;
|
||||
if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date";
|
||||
|
||||
@@ -1600,6 +1600,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A Date against an untyped value: the same Date object, whose
|
||||
// checked-dynamic handle the untyped value would hold.
|
||||
if (dynSide.type.kind === "dyn" && scalarSide.type.kind === "date") {
|
||||
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: scalarSide === left ? boxed : left,
|
||||
right: scalarSide === right ? boxed : right,
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
(scalarSide.type.kind === "f64" ||
|
||||
@@ -2024,6 +2037,28 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
if (left.type.kind === "string" || right.type.kind === "string") {
|
||||
// A Date on the left converts after the right operand is
|
||||
// evaluated, which can run its setters.
|
||||
if (left.type.kind === "date" && !isSafeToRepeat(right)) {
|
||||
const date = lowerer.declareHiddenLocal("%plus", left.type);
|
||||
const other = lowerer.declareHiddenLocal("%plus", right.type);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: date.id, init: left, loc },
|
||||
{ kind: "varDecl", localId: other.id, init: right, loc },
|
||||
],
|
||||
result: {
|
||||
kind: "strConcat",
|
||||
left: concatOperand(lowerer, varRef(date.id, left.type, loc), expr.left),
|
||||
right: concatOperand(lowerer, varRef(other.id, right.type, loc), expr.right),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "strConcat",
|
||||
left: concatOperand(lowerer, left, expr.left),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../cjs-inline-require.js";
|
||||
import { captureContextArguments } from "../function-context.js";
|
||||
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
|
||||
import { argumentCountValue } from "../argument-counts.js";
|
||||
import {
|
||||
isCjsExportTableLiteral,
|
||||
isCjsJsFile,
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
contextualUndefReadType,
|
||||
fenceEarlyAliasUse,
|
||||
isTemporalGlobalRoot,
|
||||
cjsNamespaceImportDep,
|
||||
lowerNsIdentifierValue,
|
||||
nsUndefRead,
|
||||
} from "../lower-namespaces.js";
|
||||
@@ -449,7 +451,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// semantics); the rest marker carries the trailing dyn-array ABI.
|
||||
const funcType: IrType = funcTypeFromParamShapes(sig.params, sig.returnType);
|
||||
lowerer.requireExactArityValue(expr, expr, sig.params, funcType);
|
||||
return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
|
||||
const closure = {
|
||||
kind: "closure" as const,
|
||||
fnName: sig.name,
|
||||
captures: [],
|
||||
type: funcType,
|
||||
loc,
|
||||
};
|
||||
const decl = lowerer.resolveValueSymbol(expr)?.valueDeclaration;
|
||||
return decl && ts.isFunctionDeclaration(decl) && funcType.kind === "func"
|
||||
? argumentCountValue(lowerer, decl, expr, { ...closure, type: funcType })
|
||||
: closure;
|
||||
}
|
||||
{
|
||||
// A generic function as a VALUE: monomorphized by flow — the
|
||||
@@ -972,6 +984,37 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// representation (ambient namespaces compile to Node's
|
||||
// ReferenceError instead — the object never exists at runtime).
|
||||
{
|
||||
// `ns.name` through a namespace import of a CommonJS module, where Node's
|
||||
// namespace has no `name` property (its lexer does not detect it), or
|
||||
// `ns.default` (module.exports itself).
|
||||
const cjsNamespace = cjsNamespaceImportDep(lowerer, expr);
|
||||
if (
|
||||
cjsNamespace !== null &&
|
||||
ts.isPropertyAccessExpression(expr.parent) &&
|
||||
expr.parent.expression === expr &&
|
||||
ts.isIdentifier(expr.parent.name)
|
||||
) {
|
||||
const name = expr.parent.name.text;
|
||||
const { names, assignedLater } = lowerer.cjsNamespaceOf(cjsNamespace);
|
||||
if (name === "default")
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.parent,
|
||||
`reads of 'default' through namespace imports of CommonJS modules (module.exports itself: read it through a default import)`,
|
||||
);
|
||||
if (!names.has(name))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.parent,
|
||||
`reads of '${name}' through namespace imports of CommonJS modules (Node's lexer does not detect '${name}' among the module's exports, so Node reads undefined)`,
|
||||
);
|
||||
if (assignedLater === "all" || assignedLater.has(name))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.parent,
|
||||
`reads of '${name}' through namespace imports of CommonJS modules whose functions assign exports (Node's namespace keeps the values from the end of the module's evaluation)`,
|
||||
);
|
||||
}
|
||||
const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (moduleNs?.kind === "moduleNs") {
|
||||
return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc };
|
||||
|
||||
@@ -7,6 +7,7 @@ import { lowerHistogramProperty } from "../builtins/performance.js";
|
||||
import { staticFunctionNameRead } from "../static-function-names.js";
|
||||
import { typedArgumentsArity } from "../typed-arguments.js";
|
||||
import { isThisParameter } from "../call-signatures.js";
|
||||
import { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js";
|
||||
import {
|
||||
checkedPrivateValue,
|
||||
localPrivateBrandClass,
|
||||
@@ -112,6 +113,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../.
|
||||
import { typedArrayLacksMember } from "../builtins/typed-array-members.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { DATE_METHOD_HINT } from "../builtins/dates.js";
|
||||
import {
|
||||
cjsClassExprWholeExportOf,
|
||||
cjsExportAssignmentOf,
|
||||
@@ -146,6 +148,8 @@ import { nullishGenericBindingUnitOf } from "../binding-analysis.js";
|
||||
import { blockedBindingUseDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { npmStaticPackageOfPath } from "../../npm-static.js";
|
||||
import { importMetaField, isImportMeta } from "../import-meta.js";
|
||||
import { createRequireCalleeFileOf } from "../builtins/module-bindings.js";
|
||||
import { createRequireResolveFenceValue } from "../builtins/module-resolution.js";
|
||||
import {
|
||||
builtinPrototypeMethod,
|
||||
lowerArrayIsArrayValue,
|
||||
@@ -286,6 +290,21 @@ const DGRAM_METHOD_VALUES = new Set([
|
||||
"unref",
|
||||
]);
|
||||
|
||||
function isCreateRequireResolveValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
let resolve: ts.Expression = expr;
|
||||
if (
|
||||
expr.name.text === "paths" &&
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
!expr.expression.questionDotToken
|
||||
)
|
||||
resolve = expr.expression;
|
||||
return (
|
||||
ts.isPropertyAccessExpression(resolve) &&
|
||||
resolve.name.text === "resolve" &&
|
||||
createRequireCalleeFileOf(lowerer, resolve.expression) !== null
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerPropertyAccessExpr(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
@@ -689,7 +708,31 @@ export function lowerPropertyAccessExpr(
|
||||
const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
|
||||
(p) => !isThisParameter(p),
|
||||
).length;
|
||||
const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0);
|
||||
const contextual = contextualArgumentCount(lowerer, fn);
|
||||
if (contextual === "varies") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call)",
|
||||
);
|
||||
}
|
||||
// A top-level declaration also used as a value reads the count its
|
||||
// caller stored (argument-counts.ts).
|
||||
if (ts.isFunctionDeclaration(fn) && lowerer.ctx.argumentCount !== undefined)
|
||||
return varRef(lowerer.ctx.argumentCount.id, F64, loc);
|
||||
if (
|
||||
(ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) &&
|
||||
usedAsValue(lowerer, fn)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
ts.isFunctionDeclaration(fn) && argumentCountSlot(lowerer, fn) !== null
|
||||
? "'arguments.length' in an arrow function inside a function that is also used as a value"
|
||||
: "'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares)",
|
||||
);
|
||||
}
|
||||
const count = Math.max(declared, contextual ?? 0);
|
||||
// Calls through the expression's own name pass the declared count,
|
||||
// calls through the wider slot pass more: no constant answers both.
|
||||
if (count > declared && referencesOwnName(lowerer, fn)) {
|
||||
@@ -1275,6 +1318,11 @@ export function lowerPropertyAccessExpr(
|
||||
return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read;
|
||||
}
|
||||
}
|
||||
// A createRequire loader's resolve read as a value (passed on, or
|
||||
// picked by `??`): resolution happens at build time, so only calls
|
||||
// with a literal specifier lower.
|
||||
if (isCreateRequireResolveValue(lowerer, expr))
|
||||
return createRequireResolveFenceValue(lowerer, expr, loc);
|
||||
// The lib fence's PROPERTY chokepoint: a stdlib-declared member that
|
||||
// no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
|
||||
// re.exec as a value, ...) reports SC2020 here.
|
||||
@@ -2055,6 +2103,9 @@ export function lowerIntrinsicProperty(
|
||||
);
|
||||
}
|
||||
if (kind === "date") {
|
||||
// A Date held as a checked-dynamic value (a Date a JavaScript file
|
||||
// constructs) answers its method values at run time.
|
||||
if (tryLowerExpression(lowerer, expr.expression)?.type.kind === "dyn") return null;
|
||||
const methods = new Set([
|
||||
"getTime",
|
||||
"valueOf",
|
||||
@@ -2083,7 +2134,7 @@ export function lowerIntrinsicProperty(
|
||||
lowerer.noLowering(
|
||||
`Date.prototype.${name}`,
|
||||
expr,
|
||||
"getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering",
|
||||
DATE_METHOD_HINT,
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
@@ -4558,26 +4609,9 @@ function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean {
|
||||
}
|
||||
|
||||
/** The argument count every caller passes a function expression whose
|
||||
* value flows into one fixed-arity function type, an array method's
|
||||
* callback or an annotated slot: that type's parameter count. Null when
|
||||
* there is no such type or its arity varies. */
|
||||
function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null {
|
||||
if (!ts.isFunctionExpression(fn)) return null;
|
||||
const contextual = lowerer.checker.getContextualType(fn);
|
||||
if (contextual === undefined) return null;
|
||||
const signatures = lowerer.checker.getCallSignatures(
|
||||
lowerer.checker.getNonNullableType(contextual),
|
||||
);
|
||||
if (signatures.length !== 1) return null;
|
||||
const declaration = signatures[0]!.declaration;
|
||||
if (declaration === undefined) return null;
|
||||
let count = 0;
|
||||
for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) {
|
||||
if (isThisParameter(parameter)) continue;
|
||||
if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
* value flows into a function type (argument-counts.ts contextualCallCount). */
|
||||
function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | "varies" | null {
|
||||
return ts.isFunctionExpression(fn) ? contextualCallCount(lowerer, fn) : null;
|
||||
}
|
||||
|
||||
/** The property names the program assigns on the global object, through
|
||||
|
||||
@@ -304,6 +304,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (!target) {
|
||||
const date = lowerer.isStdlibGlobal(expr.right, "Date");
|
||||
const url = lowerer.isStdlibGlobal(expr.right, "URL");
|
||||
// A catch binding holds a thrown Date as its checked-dynamic handle.
|
||||
const caughtDate = date ? lowerer.caughtLocalOf(expr.left) : null;
|
||||
if (caughtDate)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.nativeDateIs",
|
||||
args: [{ kind: "caughtToDyn", value: varRef(caughtDate.id, CAUGHT, loc), type: DYN, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if ((date || url) && !lowerer.caughtLocalOf(expr.left)) {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
if (value.type.kind === "dyn")
|
||||
@@ -1918,6 +1928,30 @@ export function lowerInExpression(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A child process has `send`, `disconnect` and `channel` only when it was
|
||||
// created with an IPC channel (fork, or an "ipc" stdio entry), and keeps
|
||||
// them after the channel closes: the runtime answers from the child.
|
||||
if (recv.type.kind === "child" && CHILD_CHANNEL_MEMBERS.has(key))
|
||||
return { kind: "libCall", fn: "child.hasChannel", args: [recv], type: BOOL, loc };
|
||||
// A native Node handle (an http.Server, a socket, a child process...):
|
||||
// a member Node's declarations give the class is on its prototype, so
|
||||
// `"closeAllConnections" in server` answers true. Undeclared names can
|
||||
// still be real (internal `_`-prefixed fields), so they keep the fence.
|
||||
if (
|
||||
NATIVE_HANDLE_KINDS.has(recv.type.kind) &&
|
||||
nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key)
|
||||
) {
|
||||
const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
return isSafeToDiscard(recv)
|
||||
? answer
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: recv, loc }],
|
||||
result: answer,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (recv.type.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2194,24 +2228,43 @@ function lowerRuntimeKeyIn(
|
||||
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** Standard-library constructors the untyped instanceof helper has no
|
||||
* instance test for: an untyped `x instanceof Object` always refuses. */
|
||||
const DYN_INSTANCEOF_REFUSED: ReadonlySet<string> = new Set(["Object", "Function"]);
|
||||
|
||||
/** The SC1090 message `x instanceof C` throws, with the site's position,
|
||||
* when `C` is a runtime constructor with no instance test. A right-hand
|
||||
* side that names one of DYN_INSTANCEOF_REFUSED always reaches that
|
||||
* refusal, so the site is listed with the runtime fences. */
|
||||
* when `C` is a runtime constructor with no instance test. */
|
||||
function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string {
|
||||
const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor");
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr.right);
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
lowerer.isStdlibSymbol(symbol) &&
|
||||
DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol))
|
||||
)
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const sf = ts.getSourceFileOfNode(expr);
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1;
|
||||
return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`;
|
||||
}
|
||||
|
||||
/** IR kinds of native Node handles whose members come from Node's own
|
||||
* declarations. */
|
||||
const NATIVE_HANDLE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"netServer",
|
||||
"netSocket",
|
||||
"httpReq",
|
||||
"httpRes",
|
||||
"httpClientReq",
|
||||
"child",
|
||||
"dgramSocket",
|
||||
"fsWatcher",
|
||||
"http2Session",
|
||||
"http2Stream",
|
||||
]);
|
||||
|
||||
/** Child process members that exist only on a child with an IPC channel. */
|
||||
const CHILD_CHANNEL_MEMBERS: ReadonlySet<string> = new Set(["send", "disconnect", "channel"]);
|
||||
|
||||
/** Whether every constituent of `t` declares a required member `key` in
|
||||
* a declaration file (Node's @types/node). */
|
||||
function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean {
|
||||
const parts = t.isUnionType() ? ts.constituentTypes(t) : [t];
|
||||
return parts.every((part) => {
|
||||
const prop = lowerer.checker.getPropertyOfType(part, key);
|
||||
if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false;
|
||||
return (
|
||||
prop.declarations.length > 0 &&
|
||||
prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
|
||||
import { isF64HandleTyped } from "../f64-handles.js";
|
||||
|
||||
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
@@ -281,13 +282,18 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
promise: "object",
|
||||
bytes: "object",
|
||||
regex: "object",
|
||||
date: "object",
|
||||
generator: "object",
|
||||
classval: "function",
|
||||
moduleNs: "object",
|
||||
undefinedT: "undefined",
|
||||
nullT: "object",
|
||||
};
|
||||
const folded = FOLD[operand.type.kind];
|
||||
// An f64 handle (a readline Interface, a Timeout) is an object.
|
||||
const folded =
|
||||
operand.type.kind === "f64" && isF64HandleTyped(lowerer, expr.expression)
|
||||
? "object"
|
||||
: FOLD[operand.type.kind];
|
||||
if (folded) {
|
||||
const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc };
|
||||
return isSafeToDiscard(operand)
|
||||
|
||||
@@ -120,6 +120,11 @@ function coercedUnion(lowerer: Lowerer, type: IrType): boolean {
|
||||
return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm));
|
||||
}
|
||||
|
||||
/** ToNumber of a Date: its time value (valueOf, the number hint). */
|
||||
function dateTimeValue(date: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc };
|
||||
}
|
||||
|
||||
/** ToNumber of a boolean: 1 for true, 0 for false. */
|
||||
function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
@@ -145,23 +150,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
const operand =
|
||||
raw.type.kind === "bool"
|
||||
? booleanToNumber(raw, loc)
|
||||
: coercedUnion(lowerer, raw.type)
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
: raw.type.kind === "date"
|
||||
? dateTimeValue(raw, loc)
|
||||
: coercedUnion(lowerer, raw.type)
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [raw],
|
||||
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
? ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumberCoerce",
|
||||
args: [raw],
|
||||
type: F64,
|
||||
loc,
|
||||
} satisfies IrExpr)
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]);
|
||||
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
|
||||
return { kind: "unary", op: "-", operand, type: F64, loc };
|
||||
@@ -185,6 +192,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
|
||||
}
|
||||
if (raw.type.kind === "bool") return booleanToNumber(raw, loc);
|
||||
// A Date's number hint answers its time value.
|
||||
if (raw.type.kind === "date") return dateTimeValue(raw, loc);
|
||||
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
|
||||
if (classNumber) return classNumber;
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Whether `node`'s value is an f64 handle: an object whose class
|
||||
* type-mapper.ts represents as an f64 id into a runtime registry (a
|
||||
* readline Interface, a Timeout or Immediate, an AsyncLocalStorage, a
|
||||
* diagnostics_channel Channel). Its checker type is an object type that
|
||||
* maps to f64; the Number wrapper interface maps to f64 for primitives. */
|
||||
export function isF64HandleTyped(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
const type = lowerer.checker.getTypeAtLocation(node);
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol !== undefined && ts.symbolName(symbol) === "Number") return false;
|
||||
return lowerer.mapTypeOf(type)?.kind === "f64";
|
||||
}
|
||||
@@ -46,6 +46,9 @@ export interface FnCtx {
|
||||
* rest param): the synthetic trailing dyn-array param `arguments`
|
||||
* reads resolve to. */
|
||||
argumentsLocal?: IrLocal | null;
|
||||
/** The count `arguments.length` reads in a top-level function declaration
|
||||
* that is also used as a value (argument-counts.ts). */
|
||||
argumentCount?: IrLocal;
|
||||
/** Arrows inherit the nearest ordinary function's arguments binding. */
|
||||
inheritsArguments?: boolean;
|
||||
/** A function lowered on demand on top of an unrelated caller's frames
|
||||
|
||||
@@ -175,6 +175,23 @@ export function declaredReturnType(
|
||||
// `() => void`), and throw-only callbacks are ordinary code
|
||||
// (`.action(() => { throw ... })`). `never` VALUES stay unmapped.
|
||||
if (retTsType.flags & ts.TypeFlags.Never) return VOID;
|
||||
// A JS generator whose result the checker cannot infer (mutually
|
||||
// recursive methods returning `yield*` of each other, as in yaml's
|
||||
// Parser) has the return type `any`: a generator over untyped values.
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
decl.asteriskToken &&
|
||||
retTsType.flags & ts.TypeFlags.Any
|
||||
)
|
||||
return {
|
||||
kind: "generator",
|
||||
...(decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
? { async: true as const }
|
||||
: {}),
|
||||
yieldT: DYN,
|
||||
retT: DYN,
|
||||
nextT: DYN,
|
||||
};
|
||||
let mappedReturn = lowerer.mapTypeOf(retTsType);
|
||||
// A generator declared to return Iterable<T> returns a generator over T.
|
||||
if (mappedReturn && decl.asteriskToken)
|
||||
|
||||
@@ -2082,6 +2082,10 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
|
||||
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
||||
(ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
|
||||
) {
|
||||
// A closure lowered here before (another call site's instance key, or
|
||||
// a repeated demand) keeps its value for the argument's own lowering.
|
||||
const known = lowerer.speculatedClosure(arg);
|
||||
if (known !== null) return known.type;
|
||||
const saved = lowerer.diagSink;
|
||||
const diagnostics: typeof lowerer.diags = [];
|
||||
lowerer.diagSink = diagnostics;
|
||||
@@ -2092,8 +2096,17 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
|
||||
lowerer.diagSink = saved;
|
||||
}
|
||||
// Direct inferred helpers can receive native callbacks even when their
|
||||
// result is an opaque handle that cannot cross checked storage.
|
||||
if (diagnostics.length === 0 && value?.type.kind === "func") return value.type;
|
||||
// result is an opaque handle that cannot cross checked storage. The
|
||||
// argument's own lowering then takes this value instead of lowering
|
||||
// the closure a second time.
|
||||
if (diagnostics.length === 0 && value?.type.kind === "func") {
|
||||
lowerer.speculatedClosures.set(arg, {
|
||||
ctx: lowerer.ctx,
|
||||
bindings: lowerer.typeParamBindings,
|
||||
value,
|
||||
});
|
||||
return value.type;
|
||||
}
|
||||
}
|
||||
if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null;
|
||||
if (ts.isAwaitExpression(arg)) {
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
/* Types of untyped JavaScript collections that their uses prove.
|
||||
*
|
||||
* In a JavaScript file, `const m = new Map()`, `new Set()` and `[]` type as
|
||||
* Map<any, any>, Set<any> and any[] to the checker, so every use lowers to
|
||||
* the checked-dynamic collection operations. When every reference of such a
|
||||
* `const` is a direct use of the collection, the value never leaves the
|
||||
* binding. If its keys, values or elements are then each one primitive type
|
||||
* wherever the program writes and looks them up, the binding lowers as the
|
||||
* typed Map<K, V>, Set<K> or T[], as a JSDoc @type would make it:
|
||||
*
|
||||
* - Map: `m.get(k)`, `m.has(k)`, `m.delete(k)`, `m.clear()`, `m.size`, and
|
||||
* `m.set(k, v)` as a statement (its result is the map).
|
||||
* - Set: `s.add(k)` as a statement, `s.has(k)`, `s.delete(k)`, `s.clear()`,
|
||||
* `s.size`.
|
||||
* - Array: `a.push(...)`, `a.pop()`, `a.shift()`, `a.at(i)`, `a.includes(v)`,
|
||||
* `a.indexOf(v)`, `a.join(separator)`, `a.length`.
|
||||
*
|
||||
* Any other reference (passing, returning, storing, exporting, iterating,
|
||||
* spreading, inspecting the collection, an optional call), a direct `eval`
|
||||
* in the file, or any doubt about a type keeps the binding untyped.
|
||||
*
|
||||
* The types come from carrier declarations in scriptc-overrides.d.ts, which
|
||||
* only the lowering program includes. */
|
||||
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { overridesDtsPath } from "../dts-paths.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
type Primitive = "string" | "number" | "boolean";
|
||||
const PRIMITIVES: readonly Primitive[] = ["string", "number", "boolean"];
|
||||
|
||||
type Collection = "map" | "set" | "array";
|
||||
|
||||
interface Candidate {
|
||||
readonly kind: Collection;
|
||||
readonly declaration: ts.VariableDeclaration;
|
||||
/** Every identifier that names the binding, its declaration's included. */
|
||||
readonly names: ts.Identifier[];
|
||||
/** Map keys or Set members written by set/add. */
|
||||
readonly keyWrites: ts.Expression[];
|
||||
/** Map values written by set. Array elements written by push. */
|
||||
readonly valueWrites: ts.Expression[];
|
||||
/** Arguments compared with keys or elements: get/has/delete, includes/indexOf. */
|
||||
readonly lookups: ts.Expression[];
|
||||
/** Array indexes passed to at(). */
|
||||
readonly indexes: ts.Expression[];
|
||||
/** Separators passed to join(). */
|
||||
readonly separators: ts.Expression[];
|
||||
key: Primitive | undefined;
|
||||
value: Primitive | undefined;
|
||||
}
|
||||
|
||||
const MAP_LOOKUPS = new Set(["get", "has", "delete"]);
|
||||
const SET_LOOKUPS = new Set(["has", "delete"]);
|
||||
const ARRAY_READS = new Set(["pop", "shift"]);
|
||||
const ARRAY_SEARCHES = new Set(["includes", "indexOf"]);
|
||||
/** The expression lowering refuses deeper nesting (SC1090). */
|
||||
const MAX_DEPTH = 200;
|
||||
|
||||
/** The inferred type of the binding a `new Map()` or `new Set()` initializes,
|
||||
* which the construction adopts. */
|
||||
export function initializedCollectionType(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
): ts.Type | null {
|
||||
const declaration = expr.parent;
|
||||
return declaration !== undefined &&
|
||||
ts.isVariableDeclaration(declaration) &&
|
||||
declaration.initializer === expr &&
|
||||
ts.isIdentifier(declaration.name)
|
||||
? inferredCollectionType(lowerer, declaration.name)
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The inferred collection type of an identifier that names a proven
|
||||
* binding, or null. Computed for every JavaScript file of the program on
|
||||
* first use. */
|
||||
export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null {
|
||||
const types = (lowerer.inferredCollectionTypes ??= inferProgram(lowerer));
|
||||
return types.size === 0 ? null : (types.get(node) ?? null);
|
||||
}
|
||||
|
||||
function inferProgram(lowerer: Lowerer): Map<ts.Node, ts.Type> {
|
||||
const out = new Map<ts.Node, ts.Type>();
|
||||
const carriers = carrierTypes(lowerer);
|
||||
if (carriers === null) return out;
|
||||
for (const file of lowerer.program.getSourceFiles()) {
|
||||
if (file.isDeclarationFile || !isJsSourceFile(file)) continue;
|
||||
for (const candidate of inferFile(lowerer, file)) {
|
||||
const name =
|
||||
candidate.kind === "map"
|
||||
? `map_${candidate.key}_${candidate.value}`
|
||||
: candidate.kind === "set"
|
||||
? `set_${candidate.key}`
|
||||
: `array_${candidate.value}`;
|
||||
const type = carriers.get(name);
|
||||
if (type === undefined) continue;
|
||||
for (const id of candidate.names) out.set(id, type);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The carrier declarations' types, by name (`map_string_number`,
|
||||
* `set_number`, `array_string`, ...), or null when the lowering program does
|
||||
* not include them. */
|
||||
function carrierTypes(lowerer: Lowerer): Map<string, ts.Type> | null {
|
||||
const file = lowerer.program.getSourceFile(overridesDtsPath());
|
||||
if (file === undefined) return null;
|
||||
const out = new Map<string, ts.Type>();
|
||||
for (const statement of file.statements.nodes) {
|
||||
if (
|
||||
!ts.isModuleDeclaration(statement) ||
|
||||
!ts.isIdentifier(statement.name) ||
|
||||
statement.name.text !== "__scriptcInferredCollections" ||
|
||||
statement.body === undefined ||
|
||||
!ts.isModuleBlock(statement.body)
|
||||
)
|
||||
continue;
|
||||
for (const member of statement.body.statements.nodes) {
|
||||
if (!ts.isVariableStatement(member)) continue;
|
||||
for (const declaration of member.declarationList.declarations.nodes)
|
||||
if (ts.isIdentifier(declaration.name))
|
||||
out.set(declaration.name.text, lowerer.checker.getTypeAtLocation(declaration.name));
|
||||
}
|
||||
}
|
||||
return out.size > 0 ? out : null;
|
||||
}
|
||||
|
||||
function inferFile(lowerer: Lowerer, file: ts.SourceFile): Candidate[] {
|
||||
const candidates = new Map<ts.Symbol, Candidate>();
|
||||
let directEval = false;
|
||||
const declarations = (node: ts.Node): void => {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "eval"
|
||||
)
|
||||
directEval = true;
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
const kind = candidateKind(lowerer, node);
|
||||
const symbol = kind !== null ? lowerer.checker.getSymbolAtLocation(node.name) : undefined;
|
||||
if (kind !== null && symbol !== undefined)
|
||||
candidates.set(symbol, {
|
||||
kind,
|
||||
declaration: node,
|
||||
names: [node.name as ts.Identifier],
|
||||
keyWrites: [],
|
||||
valueWrites: [],
|
||||
lookups: [],
|
||||
indexes: [],
|
||||
separators: [],
|
||||
key: undefined,
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
ts.walkPreorder(file, declarations);
|
||||
if (directEval || candidates.size === 0) return [];
|
||||
|
||||
const names = new Set(
|
||||
[...candidates.values()].map((c) => (c.declaration.name as ts.Identifier).text),
|
||||
);
|
||||
const refused = new Set<ts.Symbol>();
|
||||
const references = (node: ts.Node): void => {
|
||||
if (ts.isIdentifier(node) && names.has(node.text)) {
|
||||
const symbol = referencedSymbol(lowerer, node);
|
||||
const candidate = symbol !== undefined ? candidates.get(symbol) : undefined;
|
||||
if (symbol !== undefined && candidate !== undefined && node !== candidate.declaration.name) {
|
||||
if (recordUse(candidate, node)) candidate.names.push(node);
|
||||
else refused.add(symbol);
|
||||
}
|
||||
}
|
||||
};
|
||||
ts.walkPreorder(file, references);
|
||||
for (const symbol of refused) candidates.delete(symbol);
|
||||
const proven = [...candidates.values()].filter(
|
||||
(c) => c.valueWrites.length > 0 || c.keyWrites.length > 0,
|
||||
);
|
||||
solve(lowerer, proven);
|
||||
return proven.filter((c) =>
|
||||
c.kind === "map" ? c.key && c.value : c.kind === "set" ? c.key : c.value,
|
||||
);
|
||||
}
|
||||
|
||||
/** The collection a `const` declaration starts empty, when the checker
|
||||
* types it as an untyped Map, Set or array. */
|
||||
function candidateKind(lowerer: Lowerer, declaration: ts.VariableDeclaration): Collection | null {
|
||||
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) return null;
|
||||
const list = declaration.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
|
||||
const statement = list.parent;
|
||||
if (
|
||||
ts.isVariableStatement(statement) &&
|
||||
ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
||||
)
|
||||
return null;
|
||||
const init = declaration.initializer;
|
||||
let kind: Collection | null = null;
|
||||
if (ts.isArrayLiteralExpression(init) && init.elements.nodes.length === 0) kind = "array";
|
||||
else if (
|
||||
ts.isNewExpression(init) &&
|
||||
init.typeArguments === undefined &&
|
||||
(init.arguments?.nodes.length ?? 0) === 0
|
||||
) {
|
||||
if (lowerer.isStdlibGlobal(init.expression, "Map")) kind = "map";
|
||||
else if (lowerer.isStdlibGlobal(init.expression, "Set")) kind = "set";
|
||||
}
|
||||
if (kind === null) return null;
|
||||
// A JSDoc type, or anything else the checker already knows, stands.
|
||||
const type = lowerer.checker.getTypeAtLocation(declaration.name);
|
||||
const args =
|
||||
kind === "array"
|
||||
? lowerer.checker.isArrayType(type)
|
||||
? lowerer.checker.getTypeArguments(type as ts.TypeReference)
|
||||
: []
|
||||
: lowerer.checker.getTypeArguments(type as ts.TypeReference);
|
||||
const arity = kind === "map" ? 2 : 1;
|
||||
if (args.length !== arity || !args.every((a) => (a.flags & ts.TypeFlags.Any) !== 0)) return null;
|
||||
return kind;
|
||||
}
|
||||
|
||||
function referencedSymbol(lowerer: Lowerer, id: ts.Identifier): ts.Symbol | undefined {
|
||||
const parent = id.parent;
|
||||
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id)
|
||||
return lowerer.checker.getShorthandAssignmentValueSymbol(parent);
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(id);
|
||||
// `export { m }` names the export, an alias of the local binding.
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
parent !== undefined &&
|
||||
ts.isExportSpecifier(parent) &&
|
||||
parent.propertyName === undefined &&
|
||||
(symbol.flags & ts.SymbolFlags.Alias) !== 0
|
||||
)
|
||||
return lowerer.checker.getAliasedSymbol(symbol);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** Records a reference's use, or answers false when the reference is not
|
||||
* one of the collection uses this inference proves. */
|
||||
function recordUse(candidate: Candidate, id: ts.Identifier): boolean {
|
||||
const access = id.parent;
|
||||
if (
|
||||
access === undefined ||
|
||||
!ts.isPropertyAccessExpression(access) ||
|
||||
access.expression !== id ||
|
||||
access.questionDotToken !== undefined
|
||||
)
|
||||
return false;
|
||||
const name = access.name.text;
|
||||
const call = access.parent;
|
||||
if (call !== undefined && ts.isCallExpression(call) && call.expression === access) {
|
||||
if (call.questionDotToken !== undefined) return false;
|
||||
const args = call.arguments.nodes;
|
||||
if (args.some(ts.isSpreadElement)) return false;
|
||||
switch (candidate.kind) {
|
||||
case "map":
|
||||
if (MAP_LOOKUPS.has(name) && args.length === 1)
|
||||
return (candidate.lookups.push(args[0]!), true);
|
||||
if (name === "clear") return args.length === 0;
|
||||
if (name === "set" && args.length === 2 && isStatement(call)) {
|
||||
candidate.keyWrites.push(args[0]!);
|
||||
candidate.valueWrites.push(args[1]!);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case "set":
|
||||
if (SET_LOOKUPS.has(name) && args.length === 1)
|
||||
return (candidate.lookups.push(args[0]!), true);
|
||||
if (name === "clear") return args.length === 0;
|
||||
if (name === "add" && args.length === 1 && isStatement(call))
|
||||
return (candidate.keyWrites.push(args[0]!), true);
|
||||
return false;
|
||||
case "array":
|
||||
if (name === "push") return (candidate.valueWrites.push(...args), true);
|
||||
if (ARRAY_READS.has(name)) return args.length === 0;
|
||||
if (name === "at" && args.length === 1) return (candidate.indexes.push(args[0]!), true);
|
||||
if (ARRAY_SEARCHES.has(name) && args.length === 1)
|
||||
return (candidate.lookups.push(args[0]!), true);
|
||||
if (name === "join" && args.length <= 1) return (candidate.separators.push(...args), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (candidate.kind === "array" ? name === "length" : name === "size") && isPlainRead(access);
|
||||
}
|
||||
|
||||
function isStatement(call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
return node.parent !== undefined && ts.isExpressionStatement(node.parent);
|
||||
}
|
||||
|
||||
/** A read that neither writes nor deletes the property. */
|
||||
function isPlainRead(access: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = access;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === node &&
|
||||
isAssignment(parent.operatorToken.kind)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
|
||||
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
parent.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
return false;
|
||||
return !ts.isDeleteExpression(parent);
|
||||
}
|
||||
|
||||
function isAssignment(kind: ts.SyntaxKind): boolean {
|
||||
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
|
||||
/** Decides each candidate's key and value types: a fixed point in which a
|
||||
* decided candidate's reads type the expressions built from them. */
|
||||
function solve(lowerer: Lowerer, candidates: Candidate[]): void {
|
||||
const bySymbol = new Map<ts.Symbol, Candidate>();
|
||||
for (const c of candidates) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(c.declaration.name);
|
||||
if (symbol) bySymbol.set(symbol, c);
|
||||
}
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const c of candidates) {
|
||||
if (c.kind !== "array" && c.key === undefined) {
|
||||
const key = uniform(lowerer, bySymbol, c.keyWrites);
|
||||
if (key !== null) {
|
||||
c.key = key;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (c.kind !== "set" && c.value === undefined) {
|
||||
// Each candidate type is assumed for this collection's own reads.
|
||||
const accepted = PRIMITIVES.filter((assumed) => {
|
||||
c.value = assumed;
|
||||
const found = uniform(lowerer, bySymbol, c.valueWrites);
|
||||
c.value = undefined;
|
||||
return found === assumed;
|
||||
});
|
||||
if (accepted.length === 1) {
|
||||
c.value = accepted[0];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const c of candidates) {
|
||||
const element = c.kind === "array" ? c.value : c.key;
|
||||
// Lookups compare with the stored type; an index is a number.
|
||||
const lookupsAgree =
|
||||
element !== undefined &&
|
||||
c.lookups.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === element) &&
|
||||
c.indexes.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "number") &&
|
||||
c.separators.every((e) => primitiveOf(lowerer, bySymbol, e, new Set()) === "string");
|
||||
if (!lookupsAgree) {
|
||||
c.key = undefined;
|
||||
c.value = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The one primitive type of every expression, or null. */
|
||||
function uniform(
|
||||
lowerer: Lowerer,
|
||||
bySymbol: Map<ts.Symbol, Candidate>,
|
||||
exprs: readonly ts.Expression[],
|
||||
): Primitive | null {
|
||||
let found: Primitive | null = null;
|
||||
for (const e of exprs) {
|
||||
const p = primitiveOf(lowerer, bySymbol, e, new Set());
|
||||
if (p === null || (found !== null && p !== found)) return null;
|
||||
found = p;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function checkerPrimitive(type: ts.Type): Primitive | null {
|
||||
const flags = type.flags;
|
||||
if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) return "string";
|
||||
if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral)) return "number";
|
||||
if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return "boolean";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The primitive type `e` always evaluates to, or null. The checker answers
|
||||
* where it knows; where it types `e` as `any`, the value's structure does:
|
||||
* a read of a decided candidate, arithmetic and comparisons over such
|
||||
* values, and constants defined from them. An expression nested deeper than
|
||||
* the lowering's nesting limit answers null. */
|
||||
function primitiveOf(
|
||||
lowerer: Lowerer,
|
||||
bySymbol: Map<ts.Symbol, Candidate>,
|
||||
e: ts.Expression,
|
||||
visiting: Set<ts.Node>,
|
||||
depth = 0,
|
||||
): Primitive | null {
|
||||
if (depth > MAX_DEPTH) return null;
|
||||
const type = lowerer.checker.getTypeAtLocation(e);
|
||||
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0) return checkerPrimitive(type);
|
||||
const of = (x: ts.Expression): Primitive | null =>
|
||||
primitiveOf(lowerer, bySymbol, x, visiting, depth + 1);
|
||||
if (ts.isParenthesizedExpression(e)) return of(e.expression);
|
||||
if (ts.isPrefixUnaryExpression(e)) {
|
||||
if (e.operator === ts.SyntaxKind.ExclamationToken) return "boolean";
|
||||
return e.operator !== ts.SyntaxKind.PlusPlusToken &&
|
||||
e.operator !== ts.SyntaxKind.MinusMinusToken &&
|
||||
of(e.operand) === "number"
|
||||
? "number"
|
||||
: null;
|
||||
}
|
||||
if (ts.isConditionalExpression(e)) {
|
||||
const yes = of(e.whenTrue);
|
||||
return yes !== null && yes === of(e.whenFalse) ? yes : null;
|
||||
}
|
||||
if (ts.isBinaryExpression(e)) {
|
||||
const op = e.operatorToken.kind;
|
||||
switch (op) {
|
||||
case ts.SyntaxKind.LessThanToken:
|
||||
case ts.SyntaxKind.GreaterThanToken:
|
||||
case ts.SyntaxKind.LessThanEqualsToken:
|
||||
case ts.SyntaxKind.GreaterThanEqualsToken:
|
||||
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
||||
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
|
||||
case ts.SyntaxKind.EqualsEqualsToken:
|
||||
case ts.SyntaxKind.ExclamationEqualsToken:
|
||||
return "boolean";
|
||||
case ts.SyntaxKind.QuestionQuestionToken:
|
||||
case ts.SyntaxKind.BarBarToken: {
|
||||
// `m.get(k) ?? d` and `a.pop() || d`: the stored type or a default of it.
|
||||
const stored = storedRead(lowerer, bySymbol, e.left);
|
||||
const fallback = of(e.right);
|
||||
return stored !== null && stored === fallback ? stored : null;
|
||||
}
|
||||
case ts.SyntaxKind.PlusToken: {
|
||||
const left = of(e.left);
|
||||
const right = of(e.right);
|
||||
if (left === "number" && right === "number") return "number";
|
||||
return left !== null && right !== null && (left === "string" || right === "string")
|
||||
? "string"
|
||||
: null;
|
||||
}
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
case ts.SyntaxKind.AsteriskToken:
|
||||
case ts.SyntaxKind.SlashToken:
|
||||
case ts.SyntaxKind.PercentToken:
|
||||
case ts.SyntaxKind.AsteriskAsteriskToken:
|
||||
case ts.SyntaxKind.BarToken:
|
||||
case ts.SyntaxKind.AmpersandToken:
|
||||
case ts.SyntaxKind.CaretToken:
|
||||
case ts.SyntaxKind.LessThanLessThanToken:
|
||||
case ts.SyntaxKind.GreaterThanGreaterThanToken:
|
||||
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
|
||||
return of(e.left) === "number" && of(e.right) === "number" ? "number" : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (ts.isCallExpression(e) || ts.isPropertyAccessExpression(e))
|
||||
return candidateRead(lowerer, bySymbol, e);
|
||||
// A constant defined from such an expression.
|
||||
if (ts.isIdentifier(e) && !visiting.has(e)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(e);
|
||||
const declaration = symbol?.valueDeclaration;
|
||||
if (
|
||||
declaration !== undefined &&
|
||||
ts.isVariableDeclaration(declaration) &&
|
||||
declaration.initializer !== undefined &&
|
||||
ts.isVariableDeclarationList(declaration.parent) &&
|
||||
(declaration.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
) {
|
||||
visiting.add(e);
|
||||
const p = primitiveOf(lowerer, bySymbol, declaration.initializer, visiting, depth + 1);
|
||||
visiting.delete(e);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The candidate a use reads, when `e` is `c.member` or `c.member(...)` of a
|
||||
* candidate `c`. */
|
||||
function candidateOf(
|
||||
lowerer: Lowerer,
|
||||
bySymbol: Map<ts.Symbol, Candidate>,
|
||||
e: ts.Expression,
|
||||
): { candidate: Candidate; member: string } | null {
|
||||
const access = ts.isCallExpression(e) ? e.expression : e;
|
||||
if (!ts.isPropertyAccessExpression(access) || !ts.isIdentifier(access.expression)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(access.expression);
|
||||
const candidate = symbol !== undefined ? bySymbol.get(symbol) : undefined;
|
||||
return candidate !== undefined ? { candidate, member: access.name.text } : null;
|
||||
}
|
||||
|
||||
/** The type of a candidate's read that may answer undefined: `m.get(k)`,
|
||||
* `a.pop()`, `a.shift()`, `a.at(i)`. */
|
||||
function storedRead(
|
||||
lowerer: Lowerer,
|
||||
bySymbol: Map<ts.Symbol, Candidate>,
|
||||
e: ts.Expression,
|
||||
): Primitive | null {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isCallExpression(e)) return null;
|
||||
const read = candidateOf(lowerer, bySymbol, e);
|
||||
if (read === null) return null;
|
||||
const { candidate, member } = read;
|
||||
if (candidate.kind === "map" && member === "get") return candidate.value ?? null;
|
||||
if (candidate.kind === "array" && (ARRAY_READS.has(member) || member === "at"))
|
||||
return candidate.value ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The type of a candidate's read that always answers a value: size,
|
||||
* length, has, includes, indexOf, push. */
|
||||
function candidateRead(
|
||||
lowerer: Lowerer,
|
||||
bySymbol: Map<ts.Symbol, Candidate>,
|
||||
e: ts.Expression,
|
||||
): Primitive | null {
|
||||
const read = candidateOf(lowerer, bySymbol, e);
|
||||
if (read === null) return null;
|
||||
const { candidate, member } = read;
|
||||
if (!ts.isCallExpression(e)) return member === "size" || member === "length" ? "number" : null;
|
||||
if (member === "has" || member === "includes" || member === "delete") return "boolean";
|
||||
if (member === "indexOf" || (candidate.kind === "array" && member === "push")) return "number";
|
||||
if (candidate.kind === "array" && member === "join") return "string";
|
||||
return null;
|
||||
}
|
||||
@@ -51,11 +51,15 @@ import {
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
isDynTypedRefType,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { everyExpr } from "../../ir/traverse.js";
|
||||
import { checkedClassInstanceOf } from "./class-construction.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
|
||||
/** node:assert/strict binds the loose NAMES to the strict comparisons —
|
||||
* Node's own aliasing (`strict.equal === assert.strictEqual`). */
|
||||
@@ -73,6 +77,63 @@ function canonicalAssertMember(module: string, member: string): string {
|
||||
return member;
|
||||
}
|
||||
|
||||
/** The assert methods that are function values (scr_assert_method_value). */
|
||||
const ASSERT_FUNCTION_VALUES: ReadonlySet<string> = new Set([
|
||||
"ok",
|
||||
"fail",
|
||||
"equal",
|
||||
"notEqual",
|
||||
"strictEqual",
|
||||
"notStrictEqual",
|
||||
"deepEqual",
|
||||
"notDeepEqual",
|
||||
"deepStrictEqual",
|
||||
"notDeepStrictEqual",
|
||||
"partialDeepStrictEqual",
|
||||
"ifError",
|
||||
]);
|
||||
|
||||
/** The assert method a `const` binding holds (`const ok = assert.ok`,
|
||||
* `const eq = strictEqual`), or null. */
|
||||
export function assertFunctionAliasOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): { module: string; member: string } | null {
|
||||
const decl = lowerer.checker.getSymbolAtLocation(ident)?.valueDeclaration;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0 ||
|
||||
!decl.initializer
|
||||
)
|
||||
return null;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
const bi = ts.isPropertyAccessExpression(init)
|
||||
? lowerer.builtinMemberOf(init)
|
||||
: ts.isIdentifier(init)
|
||||
? lowerer.builtinImportOf(init)
|
||||
: null;
|
||||
if (!bi || (bi.module !== "assert" && bi.module !== "assert/strict")) return null;
|
||||
return ASSERT_FUNCTION_VALUES.has(canonicalAssertMember(bi.module, bi.member)) ? bi : null;
|
||||
}
|
||||
|
||||
/** An assert method read as a value (`emitter.on("x", assert.fail)`,
|
||||
* `const eq = assert.strictEqual`): an untyped function that asserts as
|
||||
* the method does, the same value for every read. Null for other
|
||||
* members. */
|
||||
export function assertFunctionValue(
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
const member = canonicalAssertMember(bi.module, bi.member);
|
||||
if (!ASSERT_FUNCTION_VALUES.has(member)) return null;
|
||||
return { kind: "libCall", fn: "assert.methodValue", args: [strLit(member, loc)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/* Node's AssertionError carries actual, expected and operator. Where the
|
||||
* verdict is computed in compiled code, the failure branch records them
|
||||
* (assert.record) before the throwing call, so a passing assertion boxes
|
||||
@@ -166,28 +227,156 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha
|
||||
);
|
||||
}
|
||||
|
||||
/** The message the runtime reads from a registered value
|
||||
* (SCR_ASSERT_VALUE_MESSAGE in scr_assert.h). */
|
||||
const VALUE_MESSAGE = "\u0001scriptc:assert-message-value";
|
||||
|
||||
/** A message argument that is not a string, met while one assertion
|
||||
* lowers (withValueMessage). `fail` marks assert.fail's message, which
|
||||
* Node uses for every value but null and undefined. */
|
||||
let valueMessage: { value: IrExpr; has: string; text: string; fail: boolean } | null = null;
|
||||
|
||||
/** The optional trailing message argument: Node accepts a string or an
|
||||
* Error (throwing the Error itself) — the STRING form lowers; a literal
|
||||
* `undefined` counts as omitted (Node's `message == null` test). Returns
|
||||
* the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the
|
||||
* runtime can distinguish an omitted message from an empty one. */
|
||||
* runtime can distinguish an omitted message from an empty one. Any other
|
||||
* value that crosses into the checked-dynamic tree (an untyped
|
||||
* `failures.join('')`, an Error) is registered with the runtime at the call
|
||||
* instead (withValueMessage). */
|
||||
function lowerMessageArg(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression | undefined,
|
||||
loc: SrcLoc,
|
||||
fail = false,
|
||||
): { msg: IrExpr; hasMsg: IrExpr } {
|
||||
if (!node || (ts.isIdentifier(node) && node.text === "undefined")) {
|
||||
return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) };
|
||||
}
|
||||
const msg = lowerer.lowerExpr(node);
|
||||
if (msg.type.kind !== "string") {
|
||||
if (msg.type.kind === "string") return { msg, hasMsg: boolLit(true, loc) };
|
||||
const value =
|
||||
valueMessage === null && (msg.type.kind === "dyn" || lowerer.dynConvertible(msg.type))
|
||||
? lowerer.coerceToExpected(msg, DYN)
|
||||
: null;
|
||||
if (value === null || value.type.kind !== "dyn") {
|
||||
lowerer.noLowering(
|
||||
`assert messages of type '${lowerer.fmt(msg.type)}'`,
|
||||
node,
|
||||
"only string messages lower (Node also accepts an Error to throw — construct and throw it directly)",
|
||||
);
|
||||
}
|
||||
return { msg, hasMsg: boolLit(true, loc) };
|
||||
const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL);
|
||||
const text = lowerer.declareHiddenLocal("assert.messageText", STRING);
|
||||
valueMessage = { value, has: has.id, text: text.id, fail };
|
||||
return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) };
|
||||
}
|
||||
|
||||
/** Lowers one assertion; a message value met on the way is evaluated after
|
||||
* the assertion's other arguments, as Node evaluates them. A null or
|
||||
* undefined message is omitted (Node's `message == null` test); a truthy
|
||||
* one is registered with the runtime, which the failure reads; any other
|
||||
* value is the empty message, which Node's comparison diffs treat as no
|
||||
* message too (createErrDiff tests the message's truthiness). */
|
||||
function withValueMessage(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
lower: () => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
const outer = valueMessage;
|
||||
valueMessage = null;
|
||||
try {
|
||||
const lowered = lower();
|
||||
const message = valueMessage as {
|
||||
value: IrExpr;
|
||||
has: string;
|
||||
text: string;
|
||||
fail: boolean;
|
||||
} | null;
|
||||
if (lowered === null || message === null) return lowered;
|
||||
// assert.ok's statements evaluate the value first; its call is last.
|
||||
const before = lowered.kind === "seqExpr" ? lowered.stmts : [];
|
||||
const call = lowered.kind === "seqExpr" ? lowered.result : lowered;
|
||||
if (call.kind !== "libCall") {
|
||||
lowerer.noLowering(
|
||||
"assert messages that are not strings in this assertion form",
|
||||
expr,
|
||||
"pass a string message",
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [...before];
|
||||
// An argument computed from the message (assert.fail's generated flag)
|
||||
// reads it after the declarations below; the others are evaluated first.
|
||||
const readsMessage = (arg: IrExpr): boolean =>
|
||||
!everyExpr(arg, {
|
||||
stmt: () => true,
|
||||
expr: (e) =>
|
||||
!(e.kind === "varRef" && (e.localId === message.has || e.localId === message.text)),
|
||||
});
|
||||
const args = call.args.map((arg, index) =>
|
||||
readsMessage(arg) ? arg : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc),
|
||||
);
|
||||
const local = lowerer.declareHiddenLocal("assert.message", DYN);
|
||||
const value = varRef(local.id, DYN, loc);
|
||||
// The runtime reports a registered value: a truthy one for the other
|
||||
// methods, any value but null and undefined for assert.fail (Node's
|
||||
// `actual == null` test), whose other values read "Failed".
|
||||
const registered: IrExpr = message.fail
|
||||
? varRef(message.has, BOOL, loc)
|
||||
: { kind: "dynTest", test: "truthy", value, type: BOOL, loc };
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: local.id, init: message.value, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.has,
|
||||
init: { kind: "dynTest", test: "nullish", negated: true, value, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.text,
|
||||
init: {
|
||||
kind: "ternary",
|
||||
cond: registered,
|
||||
then: strLit(VALUE_MESSAGE, loc),
|
||||
else_: strLit(message.fail ? "Failed" : "", loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: registered,
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "assert.messageValue",
|
||||
args: [value, { kind: "dynTest", test: "error", value, type: BOOL, loc }],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: { ...call, args }, loc },
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "libCall", fn: "assert.messageClear", args: [], type: VOID, loc },
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
} finally {
|
||||
valueMessage = outer;
|
||||
}
|
||||
}
|
||||
|
||||
/** The module-function dispatch — the spoke's entry, called from both the
|
||||
@@ -201,6 +390,15 @@ export function lowerAssertModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertMember(lowerer, expr, bi, loc));
|
||||
}
|
||||
|
||||
function lowerAssertMember(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (canonicalAssertMember(bi.module, bi.member)) {
|
||||
case "ok":
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
@@ -880,7 +1078,7 @@ export function lowerAssertDirectCall(
|
||||
'use the default import: import assert from "node:assert")',
|
||||
);
|
||||
}
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertOk(lowerer, expr, loc));
|
||||
}
|
||||
|
||||
/** assert(value[, message]) / assert.ok(value[, message]): the truthiness
|
||||
@@ -889,7 +1087,24 @@ export function lowerAssertDirectCall(
|
||||
* the same bytes at compile time). */
|
||||
function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression));
|
||||
if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) {
|
||||
if (expr.arguments.nodes.length === 0) {
|
||||
// Node's message for a call without arguments.
|
||||
const text = "No value argument passed to `assert.ok()`";
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [recordOnFailure(lowerer, boolLit(true, loc), null, boolLit(true, loc), "==", loc)],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "assert.ok",
|
||||
args: [boolLit(false, loc), strLit(text, loc), boolLit(true, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (expr.arguments.nodes.length > 2) {
|
||||
lowerer.noLowering(
|
||||
`${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`,
|
||||
expr,
|
||||
@@ -953,22 +1168,26 @@ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc)
|
||||
generated = boolLit(true, loc);
|
||||
} else {
|
||||
// Node's AssertionError sets generatedMessage to !message, so an
|
||||
// empty custom message counts as generated.
|
||||
msg = stableValue(
|
||||
lowerer,
|
||||
stmts,
|
||||
lowerMessageArg(lowerer, msgNode, loc).msg,
|
||||
"%assertFailMessage",
|
||||
loc,
|
||||
);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// empty custom message counts as generated. A message that is not a
|
||||
// string is read from the hidden local that withValueMessage declares
|
||||
// after these statements, so it is used as is.
|
||||
const { msg: lowered, hasMsg } = lowerMessageArg(lowerer, msgNode, loc, true);
|
||||
if (lowered.kind === "varRef" && hasMsg.kind === "varRef") {
|
||||
// A value message: generated when Node reads "Failed" for null or
|
||||
// undefined; the runtime sets the flag for a registered value.
|
||||
msg = lowered;
|
||||
generated = { kind: "unary", op: "!", operand: hasMsg, type: BOOL, loc };
|
||||
} else {
|
||||
msg = stableValue(lowerer, stmts, lowered, "%assertFailMessage", loc);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const falseLit: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
stmts.push(recordOnFailure(lowerer, boolLit(true, loc), null, null, "fail", loc));
|
||||
@@ -1056,9 +1275,22 @@ function lowerAssertEqual(
|
||||
// would answer false where Node can answer true (the value's own
|
||||
// earlier crossing) — a miscompare, never lowered. Bytes fence both
|
||||
// ways (the dyn copy cannot carry the Buffer/Uint8Array brand the
|
||||
// checker type knows); class instances and everything non-convertible
|
||||
// keep the fence too.
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn") {
|
||||
// checker type knows); everything non-convertible keeps the fence too.
|
||||
// A class instance crosses as a reference to the instance itself, so
|
||||
// strict compares are honest reference equality, between two instances
|
||||
// too (assert.strictEqual(e, e.setMaxListeners(1))).
|
||||
const instanceType = (t: IrType): boolean =>
|
||||
isDynTypedRefType(t) && !lowerer.isSubclassOf(t.className, "%Error");
|
||||
const instance = (e: IrExpr): boolean =>
|
||||
!deep &&
|
||||
(instanceType(e.type) ||
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ??
|
||||
false))) &&
|
||||
lowerer.dynConvertible(e.type);
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn" || (instance(a) && instance(b))) {
|
||||
const box = (e: IrExpr, node: ts.Expression): IrExpr => {
|
||||
if (e.type.kind === "dyn") return e;
|
||||
const k = e.type.kind;
|
||||
@@ -1067,6 +1299,18 @@ function lowerAssertEqual(
|
||||
k === "f64" ||
|
||||
k === "string" ||
|
||||
k === "bool" ||
|
||||
// A union of scalars and units crosses by value, like its arms.
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "bool" ||
|
||||
isUnitType(arm),
|
||||
) ??
|
||||
false)) ||
|
||||
(k === "func" && lowerer.dynConvertible(e.type)) ||
|
||||
// Runtime handles cross by REFERENCE (identity preserved), so the
|
||||
// strict compare is honest: assert.strictEqual(this, server) —
|
||||
@@ -1082,6 +1326,9 @@ function lowerAssertEqual(
|
||||
// same). The deep walk compares the retained error data without
|
||||
// re-encoding and losing identity (SEMANTICS.md).
|
||||
(k === "object" && e.type.kind === "object" && e.type.className === "%Error") ||
|
||||
instance(e) ||
|
||||
// A symbol crosses as itself, so strict compares are identity.
|
||||
(!deep && k === "symbol") ||
|
||||
(deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) ||
|
||||
// Under skipPrototype the prototype a class instance or Buffer
|
||||
// brand would carry is not compared.
|
||||
@@ -1470,17 +1717,29 @@ function classifyThrowsExpected(
|
||||
displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""),
|
||||
};
|
||||
}
|
||||
if (
|
||||
(surface === "assert.doesNotThrow" || surface === "assert.doesNotReject") &&
|
||||
expectedT?.kind === "func"
|
||||
) {
|
||||
if (expectedT?.kind === "func") {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
// An untyped value there (a dynamic-tier function's result) is checked
|
||||
// at run time like any untyped expected value.
|
||||
if (value.type.kind === "jsval" || value.type.kind === "dyn") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return { form: "errValue", value: boxed };
|
||||
}
|
||||
// A JavaScript function accepts any argument; a typed one must take
|
||||
// unknown values (the caught value need not be an Error).
|
||||
if (
|
||||
value.type.kind === "func" &&
|
||||
value.type.params.every((param) => param.kind === "dyn") &&
|
||||
(value.type.params.every((param) => param.kind === "dyn") ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node))) &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
return { form: "predicate", value: lowerer.coerceToExpected(value, DYN) };
|
||||
// The boxed function carries its source name (Node's message names
|
||||
// a named validation function).
|
||||
const fnName = jsFuncNameOf(node);
|
||||
return {
|
||||
form: "predicate",
|
||||
value: { kind: "dynFrom", value, ...(fnName !== null ? { fnName } : {}), type: DYN, loc },
|
||||
};
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`${surface} with this validation callback`,
|
||||
@@ -1546,7 +1805,9 @@ function classifyThrowsExpected(
|
||||
// tracingChannel suite's shape): the value crosses as a dyn error and
|
||||
// the runtime compares per Node's expectsError key walk. A
|
||||
// checked-dynamic expected rides directly; a static %Error boxes.
|
||||
if (expectedT?.kind === "dyn") {
|
||||
// An untyped JavaScript value (a parameter) too: a function there is
|
||||
// checked at run time (assertThrowsHelper).
|
||||
if (expectedT?.kind === "dyn" || expectedT === null) {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "dyn") return { form: "errValue", value };
|
||||
}
|
||||
@@ -1943,6 +2204,46 @@ function assertThrowsHelper(
|
||||
return { e: strLit("", loc), has: false };
|
||||
})();
|
||||
|
||||
// expectedException's function branch: a function with a prototype that
|
||||
// the caught value is an instance of passes; anything else is called as
|
||||
// a validation function, whose result must be true.
|
||||
const validateStmts = (fn: IrExpr): IrStmt[] => {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
const prototype: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: fn,
|
||||
key: strLit("prototype", loc),
|
||||
optional: true,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "undefined", negated: true, value: prototype, type: BOOL, loc },
|
||||
[
|
||||
ifStmt(
|
||||
checkedClassInstanceOf(
|
||||
lowerer,
|
||||
actual(),
|
||||
fn,
|
||||
"assert.throws against this native constructor is not supported yet",
|
||||
loc,
|
||||
),
|
||||
[doReturn],
|
||||
),
|
||||
],
|
||||
),
|
||||
lib("assert.throwsValidate", [
|
||||
actual(),
|
||||
fn,
|
||||
strLit(mode === "rejects" ? "rejects" : "throws", loc),
|
||||
m(),
|
||||
hm(),
|
||||
]),
|
||||
doReturn,
|
||||
];
|
||||
};
|
||||
|
||||
let catchBody: IrStmt[];
|
||||
if (mode === "dnr" || mode === "dnt") {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
@@ -2116,7 +2417,13 @@ function assertThrowsHelper(
|
||||
// caught value crosses as a dyn value (identity-preserving) and
|
||||
// the runtime walks the expected's keys — a mismatch throws the
|
||||
// deep-equal AssertionError.
|
||||
// A function there is a validation function (or an error class
|
||||
// value) as in the predicate form.
|
||||
catchBody = [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "function", value: varRef("ev.0", DYN, loc), type: BOOL, loc },
|
||||
validateStmts(varRef("ev.0", DYN, loc)),
|
||||
),
|
||||
lib("assert.expectsErrDyn", [
|
||||
{ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc },
|
||||
varRef("ev.0", DYN, loc),
|
||||
@@ -2127,7 +2434,8 @@ function assertThrowsHelper(
|
||||
];
|
||||
break;
|
||||
case "predicate":
|
||||
throw new InternalCompilerError("assert validation callback only enters a no-error helper");
|
||||
catchBody = validateStmts(varRef("predicate.0", DYN, loc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { lowerDateToPrimitiveCall } from "./builtins/dates.js";
|
||||
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerDynStreamMemberCall } from "./dyn-streams.js";
|
||||
@@ -595,6 +598,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
) {
|
||||
return lowerer.lowerSuperMethodCall(expr, expr.expression);
|
||||
}
|
||||
// `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript
|
||||
// class's receiver-independent method): the private method of the
|
||||
// brand-checked receiver.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
ts.isPrivateIdentifier(expr.expression.name) &&
|
||||
!expr.expression.questionDotToken
|
||||
) {
|
||||
const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression);
|
||||
if (privateCall) return privateCall;
|
||||
}
|
||||
|
||||
const consoleMember = lowerer.consoleCallMember(expr);
|
||||
const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null;
|
||||
@@ -1060,6 +1074,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
}
|
||||
const bi = lowerer.builtinImportOf(expr.expression);
|
||||
if (bi) return lowerBuiltinCall(lowerer, expr, bi, loc);
|
||||
// A constant bound to an assert method (`const ok = assert.ok`) called
|
||||
// directly is that assertion, with the message Node quotes the call for.
|
||||
{
|
||||
const alias = assertFunctionAliasOf(lowerer, expr.expression);
|
||||
const served = alias ? lowerer.lowerAssertModuleCall(expr, alias, loc) : null;
|
||||
if (served) return served;
|
||||
}
|
||||
// The assert module binding called DIRECTLY (`assert(x)` — a default
|
||||
// import or the CJS `const assert = require("assert")`): Node's
|
||||
// module object IS assert.ok; namespace-import bindings fence inside
|
||||
@@ -1824,6 +1845,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// The element spelling of an unsupported native Web method must fence
|
||||
// before lowering the callee into a checked-dynamic keyed read.
|
||||
if (ts.isElementAccessExpression(expr.expression)) {
|
||||
const datePrimitive = lowerDateToPrimitiveCall(lowerer, expr, expr.expression);
|
||||
if (datePrimitive) return datePrimitive;
|
||||
const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression);
|
||||
if (timersDispose) return timersDispose;
|
||||
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
|
||||
@@ -2464,6 +2487,9 @@ function lowerNumberConstructorValue(
|
||||
};
|
||||
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
|
||||
if (optionalNumber) return optionalNumber;
|
||||
// A Date's number hint answers its time value.
|
||||
if (arg.type.kind === "date")
|
||||
return { kind: "libCall", fn: "date.valueOf", args: [arg], type: F64, loc };
|
||||
// A program class instance converts through its own methods.
|
||||
const classNumber = classToNumberConversion(lowerer, arg, argNode);
|
||||
if (classNumber) return classNumber;
|
||||
@@ -8472,7 +8498,17 @@ function lowerObjectStaticCall(
|
||||
...(fnName !== null ? { fnName } : {}),
|
||||
};
|
||||
}
|
||||
if (target && isUnitType(target.type)) {
|
||||
// A class value whose statics are runtime properties of its constructor
|
||||
// boxes to that constructor function, as for Object.defineProperty above.
|
||||
const runtimeStaticClass =
|
||||
target?.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined;
|
||||
if (
|
||||
target &&
|
||||
(isUnitType(target.type) ||
|
||||
(runtimeStaticClass !== undefined &&
|
||||
hasRuntimeStatics(runtimeStaticClass) &&
|
||||
lowerer.dynConvertible(target.type)))
|
||||
) {
|
||||
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) };
|
||||
}
|
||||
if (target?.type.kind === "dyn") {
|
||||
@@ -8711,7 +8747,12 @@ function lowerObjectStaticCall(
|
||||
(probed && isDynTypedRefType(probed.type)) ||
|
||||
(probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) ||
|
||||
(probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) ||
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) ||
|
||||
// null, undefined and unions of untyped JavaScript values: the same
|
||||
// probe, whose ToObject throws Node's TypeError on nullish.
|
||||
(probed &&
|
||||
(isUnitType(probed.type) || probed.type.kind === "union") &&
|
||||
lowerer.dynConvertible(probed.type))
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.coerceToExpected(probed, DYN);
|
||||
@@ -11409,3 +11450,35 @@ function lowerShimIntrinsicCall(
|
||||
);
|
||||
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
|
||||
}
|
||||
|
||||
/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the
|
||||
* declaration-level class whose body declares `#m` declares it as a method:
|
||||
* the method called on the brand-checked receiver
|
||||
* (checkedPrivateMethodReceiver). Null for any other private member call. */
|
||||
function lowerCheckedPrivateMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isPrivateIdentifier(access.name)) return null;
|
||||
const name = access.name.text;
|
||||
const owner = privateNameClass(lowerer, access.name);
|
||||
const found = owner ? lowerer.findMethodOn(owner, name) : null;
|
||||
if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null;
|
||||
const probed = tryLowerExpression(lowerer, access.expression);
|
||||
if (!probed || !checkedDynamicOperand(probed)) return null;
|
||||
const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression));
|
||||
if (!value) return null;
|
||||
const loc = locOf(call);
|
||||
const callee = `%${owner.def.name}.${name}`;
|
||||
lowerer.noteEdge(callee);
|
||||
const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc);
|
||||
const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call);
|
||||
return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc };
|
||||
}
|
||||
|
||||
/** A checked-dynamic value, or the dynamic value under a checked extraction. */
|
||||
function checkedDynamicOperand(expr: IrExpr): IrExpr | null {
|
||||
if (expr.type.kind === "dyn") return expr;
|
||||
return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,18 @@ import { lowerDateStaticCall } from "./builtins/dates.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
||||
import { adaptPromiseExecutor } from "./promise-executor-adapter.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, strLit, varRef } from "../../ir/build.js";
|
||||
import {
|
||||
constructedFromValue,
|
||||
constructedValue,
|
||||
superIntoResultSlot,
|
||||
} from "./constructor-returns.js";
|
||||
import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { lowerReadlineUiNew } from "./builtins/readline.js";
|
||||
import { builtinMemberRequireDecl } from "./builtins/module-bindings.js";
|
||||
import { lowerReadlineInterfaceNew, lowerReadlineUiNew } from "./builtins/readline.js";
|
||||
import {
|
||||
builtinMemberBindingTarget,
|
||||
builtinMemberRequireDecl,
|
||||
} from "./builtins/module-bindings.js";
|
||||
import { checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { collectClassMethodMutations } from "./class-method-mutations.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
@@ -113,6 +116,7 @@ import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./low
|
||||
import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
import { initializedCollectionType } from "./js-collection-types.js";
|
||||
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
||||
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
||||
import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js";
|
||||
@@ -124,6 +128,7 @@ import {
|
||||
lowerWorkerNew,
|
||||
} from "./builtins/workers.js";
|
||||
import {
|
||||
noLoweringDiag,
|
||||
unsupportedPackageValueDiag,
|
||||
unsupportedDiag,
|
||||
type ScrDiagnostic,
|
||||
@@ -185,7 +190,7 @@ import {
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
@@ -481,6 +486,10 @@ export interface ClassInfo {
|
||||
localPrototypeData?: true;
|
||||
prototypeMethodValues?: Map<string, IrExpr>;
|
||||
prototypeReflectionReady?: true;
|
||||
/** The global that records at run time that the prototype view was
|
||||
* populated by reflection, so it holds its own `constructor` until the
|
||||
* program deletes it (prototypeReflectedFlag). */
|
||||
prototypeReflectedFlag?: string;
|
||||
prototypeObservedMethodsReady?: true;
|
||||
/** The globals holding each observed method's original prototype value
|
||||
* (set when the prototype data is built), by method name. A typed call
|
||||
@@ -5687,15 +5696,22 @@ export function lowerStaticFieldRead(
|
||||
): IrExpr | null {
|
||||
if (expr.name.text === "prototype") return null;
|
||||
if (expr.questionDotToken) return null;
|
||||
const info = staticReceiverClass(lowerer, expr.expression);
|
||||
if (!info) return null;
|
||||
return lowerClassStaticRead(lowerer, info, expr.name.text, expr.expression, expr);
|
||||
}
|
||||
|
||||
/** The class a static member read through `receiver` reads exactly: the
|
||||
* class a name binds (directly, through a require binding, or through a
|
||||
* const alias), or null. */
|
||||
export function staticReceiverClass(lowerer: Lowerer, receiver: ts.Expression): ClassInfo | null {
|
||||
if (
|
||||
!ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.kind !== ts.SyntaxKind.ThisKeyword &&
|
||||
exactClassOfReceiver(lowerer, expr.expression) === null
|
||||
!ts.isIdentifier(receiver) &&
|
||||
receiver.kind !== ts.SyntaxKind.ThisKeyword &&
|
||||
exactClassOfReceiver(lowerer, receiver) === null
|
||||
)
|
||||
return null;
|
||||
const symbol = ts.isIdentifier(expr.expression)
|
||||
? lowerer.resolveValueSymbol(expr.expression)
|
||||
: null;
|
||||
const symbol = ts.isIdentifier(receiver) ? lowerer.resolveValueSymbol(receiver) : null;
|
||||
const info =
|
||||
(symbol ? lowerer.classBySymbol.get(symbol) : undefined) ??
|
||||
// A require binding over `module.exports = class {…}` (the alias
|
||||
@@ -5703,7 +5719,7 @@ export function lowerStaticFieldRead(
|
||||
// or `C.name` below would fall through to paths that answer for
|
||||
// stdlib globals instead of this class.
|
||||
propertyAssignedClassInfoOf(lowerer, symbol) ??
|
||||
exactClassOfReceiver(lowerer, expr.expression) ??
|
||||
exactClassOfReceiver(lowerer, receiver) ??
|
||||
undefined;
|
||||
if (!info) return null;
|
||||
// A decorated name that can REBIND (a replacing decorator): the
|
||||
@@ -5712,22 +5728,36 @@ export function lowerStaticFieldRead(
|
||||
// whose devirtualization and .name rules answer for every legal
|
||||
// runtime value.
|
||||
if (info.classDecorators?.valueGlobalId !== undefined) return null;
|
||||
const loc = locOf(expr);
|
||||
const found = findStaticOn(lowerer, info, expr.name.text);
|
||||
return info;
|
||||
}
|
||||
|
||||
/** The static member `name` of `info` read through `receiver` (an exact
|
||||
* reference to the class, see staticReceiverClass): `C.name` for the
|
||||
* property access `site`, or a destructuring element over `C` whose
|
||||
* initializer is `site`. Null when the class has no such member. */
|
||||
export function lowerClassStaticRead(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
name: string,
|
||||
receiver: ts.Expression,
|
||||
site: ts.Expression,
|
||||
): IrExpr | null {
|
||||
const loc = locOf(site);
|
||||
const found = findStaticOn(lowerer, info, name);
|
||||
// A Date static read as a value through a class declared `extends Date`:
|
||||
// the statics lower only as calls.
|
||||
if (
|
||||
!found &&
|
||||
dateRootOf(info) &&
|
||||
["now", "parse", "UTC"].includes(expr.name.text) &&
|
||||
!(ts.isCallExpression(expr.parent) && expr.parent.expression === expr)
|
||||
["now", "parse", "UTC"].includes(name) &&
|
||||
!(ts.isCallExpression(site.parent) && site.parent.expression === site)
|
||||
)
|
||||
lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr);
|
||||
lowerer.noLowering(`Date.${name} read through a class that extends Date`, site);
|
||||
if (hasRuntimeStatics(info) && !found?.field)
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN),
|
||||
key: { kind: "strLit", value: name, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
@@ -5735,11 +5765,11 @@ export function lowerStaticFieldRead(
|
||||
// name: in JS the brand lives on that one constructor object, so
|
||||
// `D.#s` (a subclass receiver) throws Node's TypeError instead of
|
||||
// reaching up the chain — reported as SC1090 instead of resolved.
|
||||
if (found && expr.name.text.startsWith("#") && found.declarer !== info) {
|
||||
if (found && name.startsWith("#") && found.declarer !== info) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`reading the private static '${expr.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`,
|
||||
site,
|
||||
`reading the private static '${name}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`,
|
||||
);
|
||||
}
|
||||
if (found?.field !== undefined) {
|
||||
@@ -5749,29 +5779,31 @@ export function lowerStaticFieldRead(
|
||||
type: found.field.type,
|
||||
loc,
|
||||
};
|
||||
return found.field.runtimeOptional ? value : lowerer.maybeNarrow(value, expr);
|
||||
return found.field.runtimeOptional || !ts.isPropertyAccessExpression(site)
|
||||
? value
|
||||
: lowerer.maybeNarrow(value, site);
|
||||
}
|
||||
if (found) {
|
||||
return staticMethodValue(lowerer, found.declarer, expr.name.text, found.method, expr, loc);
|
||||
return staticMethodValue(lowerer, found.declarer, name, found.method, site, loc);
|
||||
}
|
||||
// A GENERIC static method as a VALUE: the pinned-value rule verbatim
|
||||
// (lowerGenericFnValue) — a slot spelling one concrete signature names
|
||||
// an instance, an unpinned reference fences by name.
|
||||
{
|
||||
const gfound = findGenericStaticOn(lowerer, info, expr.name.text);
|
||||
if (gfound) return lowerer.lowerGenericFnValue(expr, gfound.info);
|
||||
const gfound = findGenericStaticOn(lowerer, info, name);
|
||||
if (gfound) return lowerer.lowerGenericFnValue(site, gfound.info);
|
||||
}
|
||||
// `C.name` — the JS-observable class name, a compile-time constant on
|
||||
// the direct spelling (tsc rejects user statics named `name`, so the
|
||||
// chain above can never shadow it in TypeScript sources). A class of an
|
||||
// import cycle reads it from its class object instead, whose reference
|
||||
// checks the class's initialization (checkCycleBindingAccesses).
|
||||
if (expr.name.text === "name" && info.def.jsName !== undefined) {
|
||||
if (name === "name" && info.def.jsName !== undefined) {
|
||||
if (lowerer.classInitFlags.get(info.def.name)?.cycle === true)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "class.name",
|
||||
args: [classValueRef(lowerer, info, expr.expression)],
|
||||
args: [classValueRef(lowerer, info, receiver)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
@@ -5780,26 +5812,26 @@ export function lowerStaticFieldRead(
|
||||
if (info.callableBase)
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN),
|
||||
key: { kind: "strLit", value: name, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const data = classStaticDataFor(lowerer, info, expr.name.text, loc);
|
||||
const data = classStaticDataFor(lowerer, info, name, loc);
|
||||
if (data) {
|
||||
const value: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: data,
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
key: { kind: "strLit", value: name, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// JavaScript often assigns public statics after the class declaration.
|
||||
// Their storage stays mutable and checked, while reads retain the
|
||||
// checker's concrete type instead of losing the method receiver.
|
||||
const type = isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
const type = isJsSourceFile(ts.getSourceFileOfNode(site))
|
||||
? null
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(site));
|
||||
return type ? lowerer.coerceToExpected(value, type) : value;
|
||||
}
|
||||
return null;
|
||||
@@ -6625,7 +6657,7 @@ function staticCallOn(
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(access.expression, DYN),
|
||||
method: access.name.text,
|
||||
calleeName: access.name.text,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -9398,8 +9430,16 @@ export function lowerSuperMethodCall(
|
||||
if (!thisLocal) lowerer.unsupported("SC1080", access);
|
||||
const loc = locOf(call);
|
||||
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
||||
if (lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||
const prototype = classPrototypeData(lowerer, cls.base, loc);
|
||||
// A method whose `this` is the call-site receiver (a JavaScript class's
|
||||
// receiver-independent method value, reflectedClassMethodValue) looks the
|
||||
// method up on the base class's complete prototype, as `super` reads
|
||||
// [[HomeObject]].[[Prototype]], and calls it with that receiver.
|
||||
const dynamicThis = thisRef.type.kind === "dyn";
|
||||
if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||
const prototype =
|
||||
dynamicThis && cls.base.decl
|
||||
? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access))
|
||||
: classPrototypeData(lowerer, cls.base, loc);
|
||||
if (prototype) {
|
||||
const callee: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
@@ -9408,12 +9448,21 @@ export function lowerSuperMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// Spread arguments are spread into the dynamic call, as in a dynamic method call.
|
||||
const spreads: { arg: number; what: string }[] = [];
|
||||
const args = call.arguments.nodes.map((arg, index) => {
|
||||
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
|
||||
const spelling = ts.getTextOfNode(arg.expression);
|
||||
spreads.push({ arg: index, what: spelling });
|
||||
return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
|
||||
});
|
||||
const value: IrExpr = {
|
||||
kind: "dynCall",
|
||||
callee,
|
||||
receiver: lowerer.coerceToExpected(thisRef, DYN),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
args,
|
||||
...(spreads.length > 0 ? { spreads } : {}),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
@@ -9819,7 +9868,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
}
|
||||
const worker = lowerWorkerNew(lowerer, expr);
|
||||
if (worker) return worker;
|
||||
const readlineUi = lowerReadlineUiNew(lowerer, expr);
|
||||
const readlineUi = lowerReadlineUiNew(lowerer, expr) ?? lowerReadlineInterfaceNew(lowerer, expr);
|
||||
if (readlineUi) return readlineUi;
|
||||
const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr);
|
||||
if (channel) return channel;
|
||||
@@ -9902,34 +9951,54 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
const undefinedArg = (arg: ts.Expression): boolean =>
|
||||
(lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0;
|
||||
if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) {
|
||||
// A literal language tag: Unicode grapheme clusters have no locale
|
||||
// tailoring. Only tags of the form language[-script][-region] are
|
||||
// accepted; each is well-formed, so Node constructs the segmenter
|
||||
// without the RangeError a malformed tag throws.
|
||||
const literalLocale = (arg: ts.Expression): boolean =>
|
||||
ts.isStringLiteralLike(arg) &&
|
||||
/^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/.test(arg.text);
|
||||
if (args.length > 2 || (args[0] && !undefinedArg(args[0]) && !literalLocale(args[0]))) {
|
||||
lowerer.noLowering(
|
||||
"Intl.Segmenter locale negotiation",
|
||||
expr,
|
||||
"omit the locale or pass undefined for default Unicode grapheme segmentation",
|
||||
"omit the locale, pass undefined, or pass one language tag as a string literal",
|
||||
);
|
||||
}
|
||||
// JavaScript dependencies may construct word and sentence segmenters
|
||||
// that they use only on some paths (pi-tui's shared word segmenter):
|
||||
// the construction compiles and their segment() throws SC2020.
|
||||
// TypeScript sources keep the build-time refusal.
|
||||
const granularities = isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
? ["grapheme", "word", "sentence"]
|
||||
: ["grapheme"];
|
||||
const options = args[1];
|
||||
if (
|
||||
options &&
|
||||
!undefinedArg(options) &&
|
||||
!(
|
||||
ts.isObjectLiteralExpression(options) &&
|
||||
options.properties.nodes.every(
|
||||
(property) =>
|
||||
ts.isPropertyAssignment(property) &&
|
||||
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
||||
property.name.text === "granularity" &&
|
||||
ts.isStringLiteral(property.initializer) &&
|
||||
property.initializer.text === "grapheme",
|
||||
)
|
||||
)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"Intl.Segmenter options",
|
||||
options,
|
||||
'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported',
|
||||
);
|
||||
let granularity = "grapheme";
|
||||
if (options && !undefinedArg(options)) {
|
||||
// An object literal of `granularity` entries (none: the default; a
|
||||
// repeated key: the last one, as in JavaScript).
|
||||
if (!ts.isObjectLiteralExpression(options))
|
||||
lowerer.noLowering(
|
||||
"Intl.Segmenter options",
|
||||
options,
|
||||
'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported',
|
||||
);
|
||||
for (const property of options.properties.nodes) {
|
||||
if (
|
||||
!ts.isPropertyAssignment(property) ||
|
||||
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ||
|
||||
property.name.text !== "granularity" ||
|
||||
!ts.isStringLiteral(property.initializer) ||
|
||||
!granularities.includes(property.initializer.text)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"Intl.Segmenter options",
|
||||
options,
|
||||
'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported',
|
||||
);
|
||||
}
|
||||
granularity = property.initializer.text;
|
||||
}
|
||||
}
|
||||
// Explicit undefined expressions can carry effects; retain argument order.
|
||||
const stmts: IrStmt[] = args.map((arg) => {
|
||||
@@ -9940,7 +10009,28 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
loc: locOf(arg),
|
||||
};
|
||||
});
|
||||
const result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc };
|
||||
let result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc };
|
||||
// A word or sentence segmenter carries its segment() refusal, which
|
||||
// coverage lists as deferred to runtime.
|
||||
if (granularity !== "grapheme") {
|
||||
const surface = `Intl.Segmenter segmentation with granularity '${granularity}'`;
|
||||
lowerer.runtimeFences.push(
|
||||
noLoweringDiag(
|
||||
surface,
|
||||
loc,
|
||||
"only grapheme segmenters segment; word and sentence segmenters construct",
|
||||
),
|
||||
);
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(expr), loc.start).line;
|
||||
const refusal = `${surface} is not supported yet [SC2020 at ${loc.file}:${line + 1}]`;
|
||||
result = {
|
||||
kind: "libCall",
|
||||
fn: "intl.segmenterRefusing",
|
||||
args: [strLit(refusal, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
|
||||
}
|
||||
// A package declaration without a compiled constructor cannot provide
|
||||
@@ -10119,6 +10209,102 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const agent = lowerHttpAgentNew(lowerer, expr);
|
||||
if (agent) return agent;
|
||||
}
|
||||
// `new crypto.X509Certificate(data)` — the Dirent-style data record:
|
||||
// the certificate's lowered members (fingerprint — the SHA-1 of the
|
||||
// DER, uppercase colon-separated — plus the validFrom/validTo
|
||||
// validity window in Node's ASN1_TIME_print shape) compute AT
|
||||
// CONSTRUCTION, when Node parses too, so unparseable input throws
|
||||
// Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the
|
||||
// handle never exists. Both import forms (`crypto.X509Certificate`
|
||||
// through the namespace, named `X509Certificate`); Buffer input only
|
||||
// — the readFileSync idiom. mapType interns the matching record, so
|
||||
// locals and the composed member reads all flow. The data argument
|
||||
// feeds THREE field computations, so construction goes through an
|
||||
// interned helper whose parameter evaluates it exactly once.
|
||||
// It runs before the standard-library constructor fence below, which
|
||||
// also sees `const X = require("crypto").X509Certificate` as the
|
||||
// builtin class.
|
||||
{
|
||||
const callee = expr.expression;
|
||||
const isX509 =
|
||||
(ts.isPropertyAccessExpression(callee) &&
|
||||
callee.name.text === "X509Certificate" &&
|
||||
lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") ||
|
||||
(ts.isIdentifier(callee) &&
|
||||
(() => {
|
||||
const bi = lowerer.builtinImportOf(callee);
|
||||
return bi?.module === "crypto" && bi.member === "X509Certificate";
|
||||
})());
|
||||
if (isX509) {
|
||||
const args: readonly ts.Expression[] = expr.arguments?.nodes ?? [];
|
||||
if (args.length !== 1) {
|
||||
lowerer.noLowering(
|
||||
"X509Certificate with this argument shape",
|
||||
expr,
|
||||
"the supported form is new X509Certificate(readFileSync(path))",
|
||||
);
|
||||
}
|
||||
const data = lowerer.lowerExpr(args[0]!);
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
`X509Certificate over '${lowerer.fmt(data.type)}' data`,
|
||||
args[0]!,
|
||||
"pass the certificate Buffer or PEM string (an fs.readFileSync result)",
|
||||
);
|
||||
}
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
const key = `x509.record:${isBytes ? "bytes" : "str"}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%x509.record", key);
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const dataT = data.type;
|
||||
const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc };
|
||||
const field = (
|
||||
name: string,
|
||||
fn:
|
||||
| "crypto.x509Fingerprint"
|
||||
| "crypto.x509FingerprintStr"
|
||||
| "crypto.x509ValidFrom"
|
||||
| "crypto.x509ValidFromStr"
|
||||
| "crypto.x509ValidTo"
|
||||
| "crypto.x509ValidToStr",
|
||||
): { name: string; value: IrExpr } => ({
|
||||
name,
|
||||
value: { kind: "libCall", fn, args: [dRef], type: STRING, loc },
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "d.0", name: "d", type: dataT }],
|
||||
returnType: t,
|
||||
locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
field(
|
||||
"fingerprint",
|
||||
isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr",
|
||||
),
|
||||
field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"),
|
||||
field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"),
|
||||
],
|
||||
type: t,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [data], type: t, loc };
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isIdentifier(expr.expression) ||
|
||||
(ts.isPropertyAccessExpression(expr.expression) &&
|
||||
@@ -10132,7 +10318,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// (a no-op for every non-import= binding).
|
||||
if (ts.isIdentifier(expr.expression)) fenceEarlyAliasUse(lowerer, expr.expression, expr);
|
||||
const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name;
|
||||
const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null;
|
||||
// `const SD = require("string_decoder").StringDecoder; new SD()`
|
||||
// constructs the member, as a named import of it does.
|
||||
const symbol = builtinMemberBindingTarget(
|
||||
lowerer,
|
||||
ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null,
|
||||
);
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) {
|
||||
return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []);
|
||||
}
|
||||
@@ -10437,6 +10628,29 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
|
||||
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
|
||||
}
|
||||
// A union (`Date | string | number`) or checked-dynamic value: the
|
||||
// runtime's constructor reads a Date's time value, parses a string
|
||||
// and converts anything else to a primitive and a number.
|
||||
if (
|
||||
(arg.type.kind === "union" || arg.type.kind === "dyn") &&
|
||||
lowerer.dynConvertible(arg.type)
|
||||
) {
|
||||
const value: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "date.nativeNewValue",
|
||||
args: [
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: [lowerer.coerceToExpected(arg, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`new Date of '${lowerer.fmt(arg.type)}' values`,
|
||||
args[0]!,
|
||||
@@ -10568,7 +10782,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// expression). The CONTEXTUAL type carries the annotation — adopt
|
||||
// it when it is a supported map. TS type arguments keep winning:
|
||||
// their expression type already maps.
|
||||
if (mapped?.kind !== "map" || contextualSeed) {
|
||||
const inferred = initializedCollectionType(lowerer, expr);
|
||||
if (inferred) {
|
||||
tsType = inferred;
|
||||
mapped = lowerer.mapTypeOf(inferred);
|
||||
} else if (mapped?.kind !== "map" || contextualSeed) {
|
||||
const ctx = lowerer.checker.getContextualType(expr);
|
||||
const ctxMapped = ctx ? lowerer.mapTypeOf(ctx) : null;
|
||||
if (ctx && ctxMapped?.kind === "map") {
|
||||
@@ -10708,7 +10926,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// `new Set<T>()`: construct + bulk add from supported values,
|
||||
// including live collection cursors. Duplicates collapse in insertion order.
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
|
||||
const tsType = lowerer.typeOf(expr);
|
||||
const inferred = initializedCollectionType(lowerer, expr);
|
||||
const tsType = inferred ?? lowerer.typeOf(expr);
|
||||
let mapped = lowerer.mapTypeOf(tsType);
|
||||
const fieldType = assignedThisFieldType(lowerer, expr);
|
||||
if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType;
|
||||
@@ -10899,99 +11118,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
);
|
||||
}
|
||||
}
|
||||
// `new crypto.X509Certificate(data)` — the Dirent-style data record:
|
||||
// the certificate's lowered members (fingerprint — the SHA-1 of the
|
||||
// DER, uppercase colon-separated — plus the validFrom/validTo
|
||||
// validity window in Node's ASN1_TIME_print shape) compute AT
|
||||
// CONSTRUCTION, when Node parses too, so unparseable input throws
|
||||
// Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the
|
||||
// handle never exists. Both import forms (`crypto.X509Certificate`
|
||||
// through the namespace, named `X509Certificate`); Buffer input only
|
||||
// — the readFileSync idiom. mapType interns the matching record, so
|
||||
// locals and the composed member reads all flow. The data argument
|
||||
// feeds THREE field computations, so construction goes through an
|
||||
// interned helper whose parameter evaluates it exactly once.
|
||||
{
|
||||
const callee = expr.expression;
|
||||
const isX509 =
|
||||
(ts.isPropertyAccessExpression(callee) &&
|
||||
callee.name.text === "X509Certificate" &&
|
||||
lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") ||
|
||||
(ts.isIdentifier(callee) &&
|
||||
(() => {
|
||||
const bi = lowerer.builtinImportOf(callee);
|
||||
return bi?.module === "crypto" && bi.member === "X509Certificate";
|
||||
})());
|
||||
if (isX509) {
|
||||
const args: readonly ts.Expression[] = expr.arguments?.nodes ?? [];
|
||||
if (args.length !== 1) {
|
||||
lowerer.noLowering(
|
||||
"X509Certificate with this argument shape",
|
||||
expr,
|
||||
"the supported form is new X509Certificate(readFileSync(path))",
|
||||
);
|
||||
}
|
||||
const data = lowerer.lowerExpr(args[0]!);
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
`X509Certificate over '${lowerer.fmt(data.type)}' data`,
|
||||
args[0]!,
|
||||
"pass the certificate Buffer or PEM string (an fs.readFileSync result)",
|
||||
);
|
||||
}
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
const key = `x509.record:${isBytes ? "bytes" : "str"}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%x509.record", key);
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const dataT = data.type;
|
||||
const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc };
|
||||
const field = (
|
||||
name: string,
|
||||
fn:
|
||||
| "crypto.x509Fingerprint"
|
||||
| "crypto.x509FingerprintStr"
|
||||
| "crypto.x509ValidFrom"
|
||||
| "crypto.x509ValidFromStr"
|
||||
| "crypto.x509ValidTo"
|
||||
| "crypto.x509ValidToStr",
|
||||
): { name: string; value: IrExpr } => ({
|
||||
name,
|
||||
value: { kind: "libCall", fn, args: [dRef], type: STRING, loc },
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "d.0", name: "d", type: dataT }],
|
||||
returnType: t,
|
||||
locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
field(
|
||||
"fingerprint",
|
||||
isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr",
|
||||
),
|
||||
field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"),
|
||||
field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"),
|
||||
],
|
||||
type: t,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [data], type: t, loc };
|
||||
}
|
||||
}
|
||||
// `new X(...)` through a class VALUE (a classval-typed binding, array
|
||||
// element, map read, param): the newValue dispatch through the class
|
||||
// object's construct thunk. Arguments complete against the STATIC
|
||||
@@ -11266,13 +11392,18 @@ function genericFieldFnNodeOf(
|
||||
}
|
||||
|
||||
/** Whether a JavaScript class's own binding is the target of
|
||||
* `Object.defineProperty`/`defineProperties` in its file
|
||||
* (`Object.defineProperty(Definition, "name", { value })`): its constructor
|
||||
* `Object.defineProperty`/`defineProperties` or `Reflect.deleteProperty` in
|
||||
* its file (`Object.defineProperty(Definition, "name", { value })`,
|
||||
* `Reflect.deleteProperty(Response, "getResponseState")`): its constructor
|
||||
* then needs a live own-property table. */
|
||||
function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean {
|
||||
const name = decl.name;
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (name === undefined || !sf.text.includes("definePropert")) return false;
|
||||
if (
|
||||
name === undefined ||
|
||||
(!sf.text.includes("definePropert") && !sf.text.includes("deleteProperty"))
|
||||
)
|
||||
return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) return false;
|
||||
let found = false;
|
||||
@@ -11281,9 +11412,11 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
(node.expression.name.text === "defineProperty" ||
|
||||
(((node.expression.name.text === "defineProperty" ||
|
||||
node.expression.name.text === "defineProperties") &&
|
||||
lowerer.stdlibGlobalMember(node.expression, "Object") !== null
|
||||
lowerer.stdlibGlobalMember(node.expression, "Object") !== null) ||
|
||||
(node.expression.name.text === "deleteProperty" &&
|
||||
lowerer.stdlibGlobalMember(node.expression, "Reflect") !== null))
|
||||
) {
|
||||
const target = node.arguments.nodes[0];
|
||||
if (
|
||||
|
||||
@@ -2339,6 +2339,7 @@ export function typeofAnswer(arm: IrType): string | null {
|
||||
case "set":
|
||||
case "generator":
|
||||
case "regex":
|
||||
case "date":
|
||||
case "bytes":
|
||||
case "url":
|
||||
case "searchParams":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import { declareTypedArguments } from "./typed-arguments.js";
|
||||
import { declareArgumentCount } from "./argument-counts.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
@@ -534,12 +535,18 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
lowerer.fnStack.push(ctx);
|
||||
try {
|
||||
// The count a call through a value stored is taken (and its slot
|
||||
// cleared) before the parameters bind: a destructuring default that
|
||||
// throws must not leave the slot set for the next direct call.
|
||||
const argumentCount: IrStmt[] = [];
|
||||
declareArgumentCount(lowerer, decl, argumentCount);
|
||||
const receiver =
|
||||
decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
? declareCallReceiver(lowerer, decl, sig.generator !== undefined)
|
||||
: null;
|
||||
const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params);
|
||||
if (receiver) prologue.unshift(receiver);
|
||||
prologue.unshift(...argumentCount);
|
||||
declareFunctionArguments(
|
||||
lowerer,
|
||||
decl,
|
||||
|
||||
@@ -183,13 +183,9 @@ export function lowerGenMethodCall(
|
||||
);
|
||||
}
|
||||
arg = lowerer.lowerExpr(argNode);
|
||||
if (arg.type.kind === "date") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
".throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead)",
|
||||
);
|
||||
}
|
||||
// A Date throws as its checked-dynamic handle, the same Date object
|
||||
// (the throw statement's rule).
|
||||
if (arg.type.kind === "date") arg = lowerer.coerceToExpected(arg, DYN);
|
||||
if (arg.type.kind === "void" || arg.type.kind === "caught" || isUnitType(arg.type)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user