mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge lf/l1-whole-slot
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
name: LLVM distribution
|
||||
|
||||
# Builds the LLVM that the Linux release helpers link
|
||||
# (native/llvm-codegen/llvm-dist.mjs): LLVM from its release sources with
|
||||
# ThinLTO and native/llvm-codegen/llvm.profdata.gz, for each Linux
|
||||
# architecture. Each job builds a helper against it and one against LLVM's
|
||||
# release archive, requires the two to give identical objects on this
|
||||
# checkout's helper inputs (scripts/check-llvm-dist.mjs), prints both
|
||||
# helpers' times, and attaches the archive to the LLVM_DIST_TAG release.
|
||||
# The arm64 build uses the profile collected on x64; its job log reports how
|
||||
# many functions the profile no longer matches. Run it after a profile refresh
|
||||
# or an LLVM version change, with a new LLVM_DIST_TAG.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency: ${{ github.workflow }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build LLVM (${{ matrix.arch }})
|
||||
if: github.repository == 'vercel-labs/scriptc'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
runner: ubuntu-24.04
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# About an hour for LLVM on 4 cores, and 5 minutes for each helper link.
|
||||
timeout-minutes: 240
|
||||
permissions:
|
||||
contents: write
|
||||
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
|
||||
- name: Install LLVM's release archive and sources
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes ninja-build zlib1g-dev libzstd-dev
|
||||
base=https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" "$base/${{ matrix.llvm_asset }}.tar.xz"
|
||||
tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP"
|
||||
mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8"
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm-src.tar.xz" "$base/llvm-project-22.1.8.src.tar.xz"
|
||||
tar -xJf "$RUNNER_TEMP/llvm-src.tar.xz" -C "$RUNNER_TEMP"
|
||||
rm "$RUNNER_TEMP/llvm.tar.xz" "$RUNNER_TEMP/llvm-src.tar.xz"
|
||||
echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
- name: Build the distribution
|
||||
run: >-
|
||||
node scripts/build-llvm-dist.mjs --source "$RUNNER_TEMP/llvm-project-22.1.8.src"
|
||||
--prefix "$RUNNER_TEMP/dist/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}"
|
||||
--archive "$RUNNER_TEMP/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}.tar.xz"
|
||||
--work "$RUNNER_TEMP/llvm-dist-work"
|
||||
- name: Build a helper against each LLVM
|
||||
env:
|
||||
HELPER: llvm-linux-${{ matrix.arch }}-gnu
|
||||
run: |
|
||||
LLVM_DIR="$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" pnpm --filter "@scriptc/$HELPER" build:native
|
||||
cp "packages/$HELPER/bin/scriptc-llvm-codegen" "$RUNNER_TEMP/helper-release-archive"
|
||||
LLVM_DIR="$RUNNER_TEMP/dist/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}/lib/cmake/llvm" \
|
||||
SCRIPTC_LLVM_THINLTO=1 pnpm --filter "@scriptc/$HELPER" build:native
|
||||
cp "packages/$HELPER/bin/scriptc-llvm-codegen" "$RUNNER_TEMP/helper-profiled"
|
||||
- name: Compare the helpers
|
||||
run: >-
|
||||
node scripts/check-llvm-dist.mjs --baseline "$RUNNER_TEMP/helper-release-archive"
|
||||
--candidate "$RUNNER_TEMP/helper-profiled" --rounds 3 --work "$RUNNER_TEMP/llvm-check"
|
||||
- name: Attach the archive to the release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARCHIVE: ${{ runner.temp }}/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}.tar.xz
|
||||
run: |
|
||||
tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)")
|
||||
# Both architectures' jobs create the release if it is missing.
|
||||
gh release view "$tag" > /dev/null 2>&1 ||
|
||||
gh release create "$tag" --target "$GITHUB_SHA" --title "$tag" --latest=false \
|
||||
--notes "LLVM 22.1.8 built with ThinLTO and the helper profile, for the Linux LLVM helpers (scripts/build-llvm-dist.mjs)." ||
|
||||
gh release view "$tag" > /dev/null
|
||||
gh release upload "$tag" "$ARCHIVE" "$ARCHIVE.sha256" --clobber
|
||||
@@ -83,6 +83,7 @@ jobs:
|
||||
spawnSync('git', ['diff', '--quiet', run.head_sha, 'HEAD', '--', '.',
|
||||
':(exclude).github/workflows/release.yml', ':(exclude)scripts/verify-native-cli.mjs',
|
||||
':(exclude)scripts/package-native-cli.mjs', ':(exclude)tests/harness/native-cli-packaging.test.ts',
|
||||
':(exclude)scripts/wait-npm-available.mjs', ':(exclude)tests/harness/npm-available.test.ts',
|
||||
]).status !== 0) continue;
|
||||
const artifacts = api(`repos/${repo}/actions/runs/${run.id}/artifacts?per_page=100`).artifacts
|
||||
.filter((artifact) => !artifact.expired && artifact.size_in_bytes > 0);
|
||||
@@ -173,6 +174,7 @@ jobs:
|
||||
runtime: runtime-linux-x64-gnu
|
||||
cli: cli-linux-x64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
@@ -180,6 +182,7 @@ jobs:
|
||||
runtime: runtime-linux-arm64-gnu
|
||||
cli: cli-linux-arm64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
llvm_dist: arm64
|
||||
use_zig: true
|
||||
- platform: windows-x64
|
||||
runner: windows-2022
|
||||
@@ -195,6 +198,7 @@ jobs:
|
||||
seed_helper: llvm-linux-x64-gnu
|
||||
cli_target: x86_64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
- platform: linux-arm64-musl
|
||||
runner: ubuntu-24.04-arm
|
||||
@@ -204,12 +208,14 @@ jobs:
|
||||
seed_helper: llvm-linux-arm64-gnu
|
||||
cli_target: aarch64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
llvm_dist: arm64
|
||||
use_zig: true
|
||||
- platform: wasm32-wasi
|
||||
runner: ubuntu-24.04
|
||||
helper: llvm-linux-x64-gnu
|
||||
runtime: runtime-wasm32-wasi
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# The Intel macOS build took 24 minutes for 0.3.2 and passed 30 for 0.3.3.
|
||||
@@ -240,6 +246,28 @@ jobs:
|
||||
mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8"
|
||||
echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV"
|
||||
echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH"
|
||||
# The Linux helpers link LLVM built with ThinLTO and a profile of the
|
||||
# helper's code generation (native/llvm-codegen/llvm-dist.mjs), from a
|
||||
# release of this repository. Without that archive they link LLVM's
|
||||
# release archive, which gives the same objects more slowly. The
|
||||
# wasm32-wasi job also uploads the x64 GNU helper, so it selects the same.
|
||||
- name: Select the profiled LLVM distribution (Linux)
|
||||
if: matrix.llvm_dist != ''
|
||||
env:
|
||||
ARCH: ${{ matrix.llvm_dist }}
|
||||
run: |
|
||||
tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)")
|
||||
asset=$(node --input-type=module -e "import { llvmDistAsset } from './native/llvm-codegen/llvm-dist.mjs'; console.log(llvmDistAsset(process.env.ARCH))")
|
||||
base="https://github.com/$GITHUB_REPOSITORY/releases/download/$tag"
|
||||
if curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "$base/$asset" &&
|
||||
curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset.sha256" "$base/$asset.sha256" &&
|
||||
(cd "$RUNNER_TEMP" && sha256sum --check "$asset.sha256"); then
|
||||
tar -xJf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP"
|
||||
echo "SCRIPTC_HELPER_LLVM_DIR=$RUNNER_TEMP/${asset%.tar.xz}/lib/cmake/llvm" >> "$GITHUB_ENV"
|
||||
echo "The helper links $asset from release $tag"
|
||||
else
|
||||
echo "::warning::$asset is not available from release $tag; the helper links LLVM's release archive"
|
||||
fi
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
if: matrix.use_zig == true
|
||||
with:
|
||||
@@ -278,7 +306,15 @@ jobs:
|
||||
- name: Build native packages (POSIX)
|
||||
if: matrix.platform != 'windows-x64'
|
||||
run: |
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
if [ -n "${SCRIPTC_HELPER_LLVM_DIR:-}" ]; then
|
||||
LLVM_DIR="$SCRIPTC_HELPER_LLVM_DIR" SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/${{ matrix.helper }} build:native || {
|
||||
echo "::warning::the helper did not build against the profiled LLVM; it links LLVM's release archive"
|
||||
rm -rf node_modules/.cache/scriptc-${{ matrix.helper }}
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
}
|
||||
else
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
fi
|
||||
if [ "${{ matrix.use_zig }}" = true ]; then
|
||||
CC=zig AR=zig pnpm --filter @scriptc/${{ matrix.runtime }} build:native
|
||||
else
|
||||
@@ -394,6 +430,8 @@ jobs:
|
||||
contents: read
|
||||
actions: read
|
||||
id-token: write
|
||||
outputs:
|
||||
uploads: ${{ steps.publish.outputs.uploads }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -413,7 +451,7 @@ jobs:
|
||||
VERSION: ${{ needs.check-release.outputs.version }}
|
||||
RECEIPT_RUN: ${{ needs.check-release.outputs.receipt_run }}
|
||||
run: |
|
||||
touch "$RUNNER_TEMP/npm-accepted.txt"
|
||||
touch "$RUNNER_TEMP/npm-accepted.txt" "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
# Include this run so re-running only failed jobs restores its receipts.
|
||||
for run in "$GITHUB_RUN_ID" "$RECEIPT_RUN"; do
|
||||
if [ -z "$run" ]; then continue; fi
|
||||
@@ -422,6 +460,7 @@ jobs:
|
||||
if [ "$count" -eq 0 ]; then continue; fi
|
||||
gh run download "$run" --repo "$GITHUB_REPOSITORY" --pattern "npm-accepted-$VERSION-*" --dir "$RUNNER_TEMP/npm-receipts/$run"
|
||||
find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted.txt"
|
||||
find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted-times.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
done
|
||||
|
||||
- name: Setup pnpm
|
||||
@@ -471,6 +510,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Publish to npm
|
||||
id: publish
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -490,10 +530,14 @@ jobs:
|
||||
|
||||
# Upload acceptance and public availability are separate states.
|
||||
# Save acceptance immediately so retries never depend on registry lag.
|
||||
# npm-release.txt lists every package of the release, and
|
||||
# npm-accepted-times.txt the time npm accepted each upload, for the
|
||||
# npm-available job.
|
||||
publish_dir() {
|
||||
dir="$1"
|
||||
packed="${2:-}"
|
||||
name=$(node -p "require('./$dir/package.json').name")
|
||||
echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-release.txt"
|
||||
if grep -Fxq "$name@$VERSION" "$RUNNER_TEMP/npm-accepted.txt"; then
|
||||
echo "$name@$VERSION upload already accepted, skipping"
|
||||
return 0
|
||||
@@ -510,6 +554,7 @@ jobs:
|
||||
publish_log="$RUNNER_TEMP/npm-publish.log"
|
||||
if npm publish "$packed" $PROVENANCE --access public 2>&1 | tee "$publish_log"; then
|
||||
echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt"
|
||||
echo "$name@$VERSION $(date +%s)" >> "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
return 0
|
||||
else
|
||||
publish_status=${PIPESTATUS[0]}
|
||||
@@ -533,7 +578,8 @@ jobs:
|
||||
|
||||
# pnpm pack resolves workspace dependencies; npm handles OIDC.
|
||||
# Publish dependencies first. Registry propagation after an accepted
|
||||
# upload must not block the remaining packages or the GitHub release.
|
||||
# upload must not block the remaining packages; the npm-available
|
||||
# job waits for it.
|
||||
publish_dir packages/runtime
|
||||
publish_dir packages/runtime-darwin-arm64
|
||||
publish_dir packages/llvm-darwin-arm64
|
||||
@@ -561,23 +607,66 @@ jobs:
|
||||
publish_dir packages/cli
|
||||
echo "All package uploads accepted; npm registry propagation may continue after this job." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Each package of the release with the time its upload was accepted,
|
||||
# when a run recorded it.
|
||||
{
|
||||
echo "uploads<<NPM_UPLOADS"
|
||||
awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \
|
||||
"$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt"
|
||||
echo "NPM_UPLOADS"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Save accepted upload receipts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-accepted-${{ needs.check-release.outputs.version }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/npm-accepted.txt
|
||||
path: |
|
||||
${{ runner.temp }}/npm-accepted.txt
|
||||
${{ runner.temp }}/npm-accepted-times.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 90
|
||||
|
||||
# Publish standalone native distributions and the surface manifest after
|
||||
# npm publishing succeeds. The release body comes from the marked changelog.
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
# Waits until every package of the release resolves on the npm registry,
|
||||
# and reports each package's time from upload to available. Fails with the
|
||||
# packages that are still missing after 2 hours. It only reads from the
|
||||
# registry; the next release run retries it, and publishing skips uploads
|
||||
# npm already accepted.
|
||||
npm-available:
|
||||
name: Wait for npm availability
|
||||
needs: [check-release, publish]
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' && needs.publish.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 135
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Wait for every package to resolve
|
||||
env:
|
||||
UPLOADS: ${{ needs.publish.outputs.uploads }}
|
||||
run: |
|
||||
printf '%s\n' "$UPLOADS" > "$RUNNER_TEMP/npm-uploads.txt"
|
||||
node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-uploads.txt"
|
||||
|
||||
# Publish standalone native distributions and the surface manifest after
|
||||
# every npm package resolves. The release body comes from the marked
|
||||
# changelog.
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
needs: [check-release, publish, npm-available]
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' && needs.npm-available.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `handler-tables` | cpu | dispatch tables of functions: `Record<string, fn>` read as `t[k]!(…)`, `t[k]?.(…)` and through a checked local, a `Map<string, fn>`, an array of handlers, and an object literal of methods; some handlers declare fewer parameters than the table type |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/handler-tables.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Dispatch tables of functions: a `Record<string, fn>` of opcode handlers
|
||||
// called as `ops[name]!(…)`, `ops[name]?.(…)` and through a checked local,
|
||||
// a `Map<string, fn>` of the same handlers, an array of handlers indexed by
|
||||
// opcode number, and an object literal of methods. Some handlers declare
|
||||
// fewer parameters than the table's function type.
|
||||
type Op = (acc: number, arg: number) => number;
|
||||
|
||||
const ops: Record<string, Op> = {
|
||||
add: (acc, arg) => acc + arg,
|
||||
sub: (acc, arg) => acc - arg,
|
||||
mul: (acc, arg) => (acc * arg) % 1000003,
|
||||
neg: (acc) => -acc,
|
||||
half: (acc) => Math.floor(acc / 2),
|
||||
mix: (acc, arg) => (acc ^ arg) & 0xffff,
|
||||
};
|
||||
|
||||
const byName = new Map<string, Op>(Object.entries(ops));
|
||||
const byCode: Op[] = Object.values(ops);
|
||||
const names = Object.keys(ops);
|
||||
|
||||
const methods = {
|
||||
add(acc: number, arg: number): number {
|
||||
return acc + arg;
|
||||
},
|
||||
sub(acc: number, arg: number): number {
|
||||
return acc - arg;
|
||||
},
|
||||
mul(acc: number, arg: number): number {
|
||||
return (acc * arg) % 1000003;
|
||||
},
|
||||
};
|
||||
|
||||
interface Insn {
|
||||
name: string;
|
||||
code: number;
|
||||
arg: number;
|
||||
}
|
||||
|
||||
function program(length: number): Insn[] {
|
||||
const out: Insn[] = [];
|
||||
let seed = 12345;
|
||||
for (let i = 0; i < length; i++) {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
const code = seed % (names.length + 1);
|
||||
// One in seven instructions names an opcode no table holds.
|
||||
out.push({ name: code < names.length ? names[code]! : "nop", code, arg: (seed >> 8) % 97 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runAsserted(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.name !== "nop") acc = ops[insn.name]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runOptional(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) acc = ops[insn.name]?.(acc, insn.arg) ?? acc + 1;
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runChecked(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = ops[insn.name];
|
||||
if (handler) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMap(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = byName.get(insn.name);
|
||||
if (handler !== undefined) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runArray(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.code < byCode.length) acc = byCode[insn.code]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMethods(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
if (insn.code === 0) acc = methods.add(acc, insn.arg);
|
||||
else if (insn.code === 1) acc = methods.sub(acc, insn.arg);
|
||||
else if (insn.code === 2) acc = methods["mul"](acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "4");
|
||||
const insns = program(200_000);
|
||||
const totals = [0, 0, 0, 0, 0, 0];
|
||||
for (let round = 0; round < rounds * 10; round++) {
|
||||
totals[0] = (totals[0]! + runAsserted(insns)) % 1e9;
|
||||
totals[1] = (totals[1]! + runOptional(insns)) % 1e9;
|
||||
totals[2] = (totals[2]! + runChecked(insns)) % 1e9;
|
||||
totals[3] = (totals[3]! + runMap(insns)) % 1e9;
|
||||
totals[4] = (totals[4]! + runArray(insns)) % 1e9;
|
||||
totals[5] = (totals[5]! + runMethods(insns)) % 1e9;
|
||||
}
|
||||
console.log(totals.join(" "));
|
||||
@@ -2,7 +2,7 @@
|
||||
title: "CLI Reference"
|
||||
---
|
||||
|
||||
The `scriptc` command has five subcommands: `build`, `run`, `coverage`, `cache warm`, and `merge-profiles`. `scriptc --help` prints this summary and the options below, and `scriptc --version` prints the installed version.
|
||||
The `scriptc` command has six subcommands: `build`, `run`, `coverage`, `types`, `cache warm`, and `merge-profiles`. `scriptc --help` prints this summary and the options below, and `scriptc --version` prints the installed version.
|
||||
|
||||
```console
|
||||
$ scriptc --help
|
||||
@@ -21,6 +21,13 @@ Usage:
|
||||
exporting the profile symbols; a profile
|
||||
with a sidecar section also gets the
|
||||
contract sidecar JSON beside the archive
|
||||
scriptc types patch <package> <selector> <type> [--range <r>] [--dry-run]
|
||||
fix a wrong declaration of an installed
|
||||
package: shows what the selector
|
||||
matches and adds the type patch to
|
||||
package.json ("| undefined" widens)
|
||||
scriptc types list the project's type patches and what
|
||||
they match
|
||||
scriptc cache warm [runtime|tls…] verify and load precompiled runtime families
|
||||
for the current compiler/SDK/target
|
||||
scriptc merge-profiles -o <out.profdata> [--sparse] <profile…>
|
||||
@@ -88,6 +95,24 @@ $ scriptc run fib.ts
|
||||
|
||||
Analyzes the program without building it and reports what compiles and what blocks the rest. See [Coverage Reports](/docs/coverage).
|
||||
|
||||
## scriptc types
|
||||
|
||||
Writes and lists [type patches](/docs/dependencies#fix-a-packages-types), the project's fixes for wrong declarations of npm packages.
|
||||
|
||||
<dl>
|
||||
<dt><code>scriptc types patch <package> <selector> <type></code></dt>
|
||||
<dd>Finds the package installed for the working directory, prints every declaration the selector matches in its declaration files, its JSDoc and its <code>@types/</code> package with the type before and after, and adds the patch to the nearest <code>package.json</code>. The patch joins an entry whose range covers the installed version, or gets the range <code>^<installed version></code>. Nothing is written when the selector matches nothing. <code>--range <range></code> chooses the range, and <code>--dry-run</code> only prints.</dd>
|
||||
<dt><code>scriptc types list</code></dt>
|
||||
<dd>Prints each type patch in the nearest <code>package.json</code>, whether it applies to the installed version, and what it matches.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
$ scriptc types list
|
||||
commander@^15.0.0
|
||||
HelpContext.helpWidth: | undefined
|
||||
node_modules/commander/lib/command.js:2481 number -> number | undefined
|
||||
```
|
||||
|
||||
## scriptc cache warm
|
||||
|
||||
Verifies and loads the installed precompiled runtime objects and vendored libraries, and prints the cache directory. Pass `runtime`, `tls`, or both to select which to warm. Ordinary builds don't need this step. With `--sanitize`, it builds the instrumented runtime objects using a C toolchain.
|
||||
@@ -138,6 +163,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<dt><code>--target-cpu <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native></code></dt>
|
||||
<dd>On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, <code>baseline</code>, runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level. <code>native</code> uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See <a href="#target-cpu">Target CPU</a>.</dd>
|
||||
|
||||
<dt><code>--cpu-dispatch</code></dt>
|
||||
<dd>On x86-64 targets, compile the program's code twice, for the baseline and for <code>x86-64-v3</code>, and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the <code>x86-64-v3</code> code on CPUs that support it. Not used with a <code>--target-cpu</code> level above <code>baseline</code>. Valid only for executable builds. See <a href="#runtime-cpu-dispatch">Runtime CPU dispatch</a>.</dd>
|
||||
|
||||
<dt><code>--profile-generate <pattern></code></dt>
|
||||
<dd>On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where <code>%p</code> expands to the process ID; the <code>LLVM_PROFILE_FILE</code> environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
|
||||
@@ -271,6 +299,22 @@ An executable built for a level runs only on CPUs that support that level. On an
|
||||
|
||||
The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option.
|
||||
|
||||
### Runtime CPU dispatch
|
||||
|
||||
`--cpu-dispatch` keeps the executable portable and still uses the higher level where the CPU has it. The program's functions are compiled for the baseline and for `x86-64-v3`. At startup, the executable checks the CPU and the operating system's register support, and then runs the `x86-64-v3` code on CPUs that support every feature of that level. Calls between the program's functions stay within the chosen copy.
|
||||
|
||||
```console
|
||||
$ scriptc build app.ts --optimization=speed --cpu-dispatch -o app
|
||||
```
|
||||
|
||||
The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds.
|
||||
|
||||
The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code.
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CPU_LEVEL=baseline ./app
|
||||
```
|
||||
|
||||
## LLVM backend
|
||||
|
||||
scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use <code>--emit=llvm</code> to inspect the generated code.
|
||||
|
||||
@@ -145,7 +145,24 @@ A JavaScript package can contain operations scriptc can't compile, often in func
|
||||
|
||||
scriptc trusts declared types: a package's `.d.ts` files, its `@types/` package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a `TypeError` instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared `string` or `number` that is sometimes `undefined`.
|
||||
|
||||
Fix such a declaration in your project with a type patch in `package.json`:
|
||||
When the type comes from a package's declarations, the error names the declaration and the patch that would admit the value:
|
||||
|
||||
```text
|
||||
Uncaught TypeError: expected string at $, got undefined
|
||||
profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
|
||||
if that declaration is wrong, patch it: scriptc types patch profilekit "Profile.email" "| undefined"
|
||||
```
|
||||
|
||||
Fix such a declaration in your project with a type patch. `scriptc types patch` shows what a declaration path matches in the installed package and adds the patch to your `package.json`:
|
||||
|
||||
```console
|
||||
$ scriptc types patch commander "HelpContext.helpWidth" "| undefined"
|
||||
commander 15.0.0 (node_modules/commander)
|
||||
lib/command.js:2481 number -> number | undefined
|
||||
wrote package.json scriptc.typePatches["commander@^15.0.0"].declarations["HelpContext.helpWidth"]
|
||||
```
|
||||
|
||||
The patch it writes looks like this:
|
||||
|
||||
```json:package.json
|
||||
{
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
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.
|
||||
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.
|
||||
It may also be a source function, which the pipeline calls with `{ signal }` and whose iterable result it pumps, or a `{ readable, writable }` pair, whose readable side it pumps.
|
||||
Later `pipeline` stages that arrive as untyped values may be native streams, `Cipheriv` and `Decipheriv` values, functions, and iterables.
|
||||
A function is called with the previous stage and `{ signal }` as a function stage: before the last stage it must return an async iterable, and as the last stage it may return a promise, whose value resolves the pipeline, or an async iterable, which feeds the PassThrough that `pipeline()` returns.
|
||||
An untyped source function, function stage, or destination that returns a stream is unsupported.
|
||||
An iterable becomes `Duplex.from` of it, which the next stage reads from while the stages before it are not read, as in Node.
|
||||
A pipeline branches at run time on whether each of its first three untyped stages after the first is a function; a function in a later untyped stage, an untyped web stream, and a `{ readable, writable }` pair after the first stage throw a not-supported error.
|
||||
`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.
|
||||
|
||||
@@ -2,12 +2,18 @@ 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.
|
||||
A package that looks for an optional addon at run time compiles with `--npm-static`.
|
||||
In JavaScript dependencies, a `createRequire` require call with a computed specifier, and `require.resolve` called with a computed request or read as a value, resolve the specifier with the Node.js algorithm when the program runs.
|
||||
`require.resolve` returns the path Node.js returns.
|
||||
When nothing resolves, both throw the `MODULE_NOT_FOUND` error that Node.js throws, so a package that probes for a missing addon takes the same branch as in Node.js.
|
||||
When the specifier of a require call resolves to a native addon, a builtin, or a file that is not part of the compiled program, the program prints an SC2020 error that names it and exits with status 1, because Node.js would load that module.
|
||||
A `catch` clause around the call does not run in that case.
|
||||
Resolution steps that the runtime does not reproduce, such as package `imports` specifiers, `require.resolve` options, and subpaths that a package does not export, end the program the same way.
|
||||
Coverage lists these calls as deferred to runtime.
|
||||
Other computed uses compile when they are inside a `try` block: a require call with spread arguments, a `require.resolve` call with another argument count, and `require.resolve.paths` called with a computed request evaluate their arguments and then throw an error, and `require.resolve.paths` read as a value is a function that throws when called.
|
||||
Coverage lists these as deferred to runtime as well, and the package's `catch` clause then runs as it does in Node.js when the addon is missing.
|
||||
Outside a `try` block, these uses keep the package refused.
|
||||
This applies until compiled programs can load addons through Node.js.
|
||||
|
||||
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.
|
||||
|
||||
@@ -17,6 +17,14 @@ set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING
|
||||
# Linux packages ship this process-isolated helper as a fully static host
|
||||
# executable so it runs independently of the destination libc and libstdc++.
|
||||
option(SCRIPTC_STATIC_LINUX_BUILD "Build a static Linux helper executable" OFF)
|
||||
# The release Linux helpers link an LLVM built with ThinLTO and a profile of
|
||||
# the helper's code generation (llvm-dist.mjs). Its archives hold bitcode, so
|
||||
# the helper compiles and links with ThinLTO too, which needs clang and lld.
|
||||
option(SCRIPTC_LLVM_THINLTO "Compile and link the helper with ThinLTO" OFF)
|
||||
set(SCRIPTC_LLVM_PROFILE_USE "" CACHE FILEPATH
|
||||
"Indexed profile the helper's own sources are optimized with")
|
||||
option(SCRIPTC_LLVM_PROFILE_GENERATE
|
||||
"Instrument the helper's sources to collect that profile" OFF)
|
||||
|
||||
# These must be cache variables before project() enables the Apple toolchain.
|
||||
# A target property named OSX_DEPLOYMENT_TARGET is not recognized by CMake and
|
||||
@@ -219,6 +227,31 @@ else()
|
||||
endif()
|
||||
target_link_libraries(scriptc-llvm-codegen PRIVATE ${SCRIPTC_LLVM_LIBS} ${SCRIPTC_LLVM_SYSTEM_LIBS})
|
||||
|
||||
if(SCRIPTC_LLVM_THINLTO)
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE -flto=thin)
|
||||
# The thin link optimizes and compiles all of LLVM's code the helper
|
||||
# reaches; the cache makes a relink after a helper edit incremental.
|
||||
target_link_options(scriptc-llvm-codegen PRIVATE
|
||||
-flto=thin
|
||||
-fuse-ld=lld
|
||||
"LINKER:--thinlto-jobs=all"
|
||||
"LINKER:--thinlto-cache-dir=${CMAKE_BINARY_DIR}/thinlto-cache"
|
||||
)
|
||||
endif()
|
||||
if(SCRIPTC_LLVM_PROFILE_USE)
|
||||
# The profile covers LLVM's sources and the helper's. Functions it does
|
||||
# not cover, or whose control flow changed since, are optimized without it.
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE
|
||||
"-fprofile-use=${SCRIPTC_LLVM_PROFILE_USE}"
|
||||
-Wno-profile-instr-unprofiled
|
||||
-Wno-profile-instr-out-of-date
|
||||
)
|
||||
endif()
|
||||
if(SCRIPTC_LLVM_PROFILE_GENERATE)
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE -fprofile-generate)
|
||||
target_link_options(scriptc-llvm-codegen PRIVATE -fprofile-generate)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Homebrew's static LLVM archives contain broad component object files.
|
||||
# Export only the process entry point so archive-global LLVM symbols do not
|
||||
|
||||
@@ -37,7 +37,7 @@ Repeating `--output` divides the program into that many partitions, which are co
|
||||
|
||||
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
|
||||
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect. Partitions drop the names of their struct types before the thin link, which would otherwise number same-named types from different partitions in an order that any edit can shift, changing the module after importing in partitions the edit did not touch.
|
||||
|
||||
`--import-bitcode <unit.bc>`, given once per runtime-pack unit that ships bitcode, imports runtime functions for inlining; scriptc's program builds do not pass it. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
|
||||
|
||||
@@ -58,3 +58,20 @@ scriptc-llvm-codegen prune-import --input unit.opt.bc --output unit.bc
|
||||
```
|
||||
|
||||
Both commands keep only what a program build can import: the importable bodies within the size limit at their call distance from any public function, and the constants they copy. Every other definition becomes a declaration, so a program imports exactly what it would from the whole unit while reading a fraction of it. Unpromoted unit-local symbols stay local, so release bitcode offers only bodies that need none of them.
|
||||
|
||||
## Linux release builds
|
||||
|
||||
The Linux helper packages are released linked against LLVM 22.1.8 built from its release sources with ThinLTO and with `llvm.profdata.gz`, a profile of the helper's own code generation (`llvm-dist.mjs`). The helper's code generation is 5-9% faster with it than when the helper links LLVM's release archive, and its objects are identical. The release workflow downloads that build from the `LLVM_DIST_TAG` release of this repository; when the archive is missing, the helper links LLVM's release archive instead.
|
||||
|
||||
To build and use it locally, with clang, lld and llvm-profdata 22.1.8 on `PATH` (LLVM's release archive for the host has them):
|
||||
|
||||
```console
|
||||
$ node scripts/build-llvm-dist.mjs --source llvm-project-22.1.8.src --prefix <dir>
|
||||
$ LLVM_DIR=<dir>/lib/cmake/llvm SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/llvm-linux-x64-gnu build:native
|
||||
```
|
||||
|
||||
`SCRIPTC_LLVM_THINLTO=1` compiles and links the helper with clang, lld and ThinLTO, since that LLVM's archives hold bitcode, and optimizes the helper's own sources with the same profile. The link takes about 4 minutes on 4 cores.
|
||||
|
||||
The `llvm-dist` workflow (run by hand) builds the archive for x64 and arm64, checks that a helper linked against it gives the same objects as one linked against LLVM's release archive (`scripts/check-llvm-dist.mjs`), and attaches it to the release. The arm64 build uses the profile collected on x64, and its log reports how many functions the profile does not match there.
|
||||
|
||||
The profile depends on LLVM's sources and the kind of code scriptc emits, not on scriptc's commits. `scripts/refresh-llvm-profile.mjs` collects a new one after an LLVM version change: it builds LLVM with IR instrumentation and an instrumented helper, runs that helper on tsc-ts, five Effect fixtures, the runtime benchmark workloads and every 50th corpus program (`scripts/llvm-helper-inputs.mjs`), and merges the result. A new profile or LLVM version gets a new `LLVM_DIST_TAG`.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* The static Linux helper build shared by the four Linux helper packages,
|
||||
* scripts/refresh-llvm-profile.mjs and scripts/check-llvm-dist.mjs. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { helperLlvmDistArgs } from "./llvm-dist.mjs";
|
||||
import { nativeTargetCmakeArgs } from "./targets.mjs";
|
||||
|
||||
const DATA_LAYOUTS = {
|
||||
x64: "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
|
||||
arm64:
|
||||
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32",
|
||||
};
|
||||
|
||||
/** Configures and builds the helper for `arch` (`x64` or `arm64`) and `libc`
|
||||
* (`gnu` or `musl`) in `build` against LLVM_DIR, and writes the stripped
|
||||
* executable to `output`. musl builds take the compilers from CC and CXX. */
|
||||
export function buildLinuxHelper({ arch, libc, version, build, output }) {
|
||||
const triple = `${arch === "x64" ? "x86_64" : "aarch64"}-unknown-linux-${libc}`;
|
||||
// CMake drops the other definitions when a reconfigure changes the
|
||||
// compiler, so a build directory configured for another LLVM or build
|
||||
// kind starts over.
|
||||
const kind = JSON.stringify({
|
||||
llvm: process.env.LLVM_DIR ?? null,
|
||||
thinLTO: process.env.SCRIPTC_LLVM_THINLTO === "1",
|
||||
profileGenerate: process.env.SCRIPTC_LLVM_PROFILE_GENERATE === "1",
|
||||
cc: process.env.CC ?? null,
|
||||
cxx: process.env.CXX ?? null,
|
||||
});
|
||||
const stamp = join(build, "scriptc-helper-build.json");
|
||||
if (existsSync(build) && (!existsSync(stamp) || readFileSync(stamp, "utf8") !== kind))
|
||||
rmSync(build, { recursive: true, force: true });
|
||||
mkdirSync(build, { recursive: true });
|
||||
writeFileSync(stamp, kind);
|
||||
execFileSync(
|
||||
"cmake",
|
||||
[
|
||||
"-S",
|
||||
fileURLToPath(new URL(".", import.meta.url)),
|
||||
"-B",
|
||||
build,
|
||||
"-G",
|
||||
"Ninja",
|
||||
...(libc === "musl"
|
||||
? [
|
||||
`-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`,
|
||||
`-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`,
|
||||
]
|
||||
: []),
|
||||
`-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`,
|
||||
`-DSCRIPTC_PACKAGE_VERSION=${version}`,
|
||||
`-DSCRIPTC_DEFAULT_TARGET=${triple}`,
|
||||
`-DSCRIPTC_DEFAULT_DATA_LAYOUT=${DATA_LAYOUTS[arch]}`,
|
||||
...nativeTargetCmakeArgs,
|
||||
"-DSCRIPTC_STATIC_LINUX_BUILD=ON",
|
||||
"-DCMAKE_EXE_LINKER_FLAGS=-static",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
// Later definitions win, so these can select clang for musl builds.
|
||||
...helperLlvmDistArgs(build),
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
copyFileSync(join(build, "scriptc-llvm-codegen"), output);
|
||||
execFileSync("strip", [output], { stdio: "inherit" });
|
||||
chmodSync(output, 0o755);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/* The LLVM distribution the Linux helpers link in releases: LLVM built from
|
||||
* the release sources with ThinLTO and with a profile of the helper's own
|
||||
* code generation (llvm.profdata.gz). scripts/build-llvm-dist.mjs builds it,
|
||||
* the llvm-dist workflow attaches it to the LLVM_DIST_TAG release of this
|
||||
* repository, and scripts/refresh-llvm-profile.mjs collects the profile. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
|
||||
export const LLVM_VERSION = "22.1.8";
|
||||
/** The release of this repository that holds the distribution archives. A
|
||||
* new profile or LLVM version gets a new tag. */
|
||||
export const LLVM_DIST_TAG = "llvm-22.1.8-pgo.1";
|
||||
export const llvmProfile = fileURLToPath(new URL("./llvm.profdata.gz", import.meta.url));
|
||||
|
||||
/** The archive name for a Linux architecture (`x64` or `arm64`). */
|
||||
export function llvmDistAsset(arch) {
|
||||
return `scriptc-llvm-${LLVM_VERSION}-linux-${arch}.tar.xz`;
|
||||
}
|
||||
|
||||
/** Writes the uncompressed profile into `dir` and returns its path. */
|
||||
export function writeLlvmProfile(dir, profile = llvmProfile) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const path = join(dir, "llvm.profdata");
|
||||
writeFileSync(path, gunzipSync(readFileSync(profile)));
|
||||
return path;
|
||||
}
|
||||
|
||||
/** CMake arguments for an LLVM build with the helper's targets, static,
|
||||
* without tools or tests. `profile` builds with that indexed profile and
|
||||
* ThinLTO; `instrumented` builds the IR-instrumented libraries the profile
|
||||
* is collected with. clang and lld come from PATH and must be LLVM_VERSION,
|
||||
* since the profile's format and hashes follow the compiler. */
|
||||
export function llvmCmakeArgs({ source, build, prefix, profile, instrumented }) {
|
||||
const version = execFileSync("clang", ["--version"], { encoding: "utf8" });
|
||||
if (!version.includes(`clang version ${LLVM_VERSION}`))
|
||||
throw new Error(`the LLVM distribution is built with clang ${LLVM_VERSION}; found ${version}`);
|
||||
return [
|
||||
"-S",
|
||||
join(source, "llvm"),
|
||||
"-B",
|
||||
build,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
`-DCMAKE_INSTALL_PREFIX=${prefix}`,
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DLLVM_USE_LINKER=lld",
|
||||
"-DLLVM_TARGETS_TO_BUILD=X86;AArch64;WebAssembly",
|
||||
"-DLLVM_ENABLE_PROJECTS=",
|
||||
"-DLLVM_ENABLE_ASSERTIONS=OFF",
|
||||
"-DLLVM_ENABLE_ZLIB=FORCE_ON",
|
||||
"-DLLVM_ENABLE_ZSTD=FORCE_ON",
|
||||
"-DLLVM_USE_STATIC_ZSTD=ON",
|
||||
"-DLLVM_ENABLE_TERMINFO=OFF",
|
||||
"-DLLVM_ENABLE_LIBXML2=OFF",
|
||||
"-DLLVM_ENABLE_LIBEDIT=OFF",
|
||||
"-DLLVM_ENABLE_LIBPFM=OFF",
|
||||
"-DLLVM_INCLUDE_TESTS=OFF",
|
||||
"-DLLVM_INCLUDE_EXAMPLES=OFF",
|
||||
"-DLLVM_INCLUDE_BENCHMARKS=OFF",
|
||||
"-DLLVM_INCLUDE_DOCS=OFF",
|
||||
"-DLLVM_BUILD_TOOLS=OFF",
|
||||
...(instrumented ? ["-DLLVM_BUILD_INSTRUMENTED=IR", "-DLLVM_BUILD_RUNTIME=OFF"] : []),
|
||||
...(profile ? ["-DLLVM_ENABLE_LTO=Thin", `-DLLVM_PROFDATA_FILE=${profile}`] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/** Extra CMake arguments for a Linux helper package build. With
|
||||
* SCRIPTC_LLVM_THINLTO=1, LLVM_DIR names a distribution from
|
||||
* scripts/build-llvm-dist.mjs: its archives hold ThinLTO bitcode, so the
|
||||
* helper compiles and links with clang, lld and ThinLTO, and its own sources
|
||||
* use the same profile. SCRIPTC_LLVM_PROFILE_GENERATE=1 instruments the
|
||||
* helper's sources against an instrumented LLVM. */
|
||||
export function helperLlvmDistArgs(build) {
|
||||
const args = [];
|
||||
if (process.env.SCRIPTC_LLVM_THINLTO === "1")
|
||||
args.push(
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DSCRIPTC_LLVM_THINLTO=ON",
|
||||
`-DSCRIPTC_LLVM_PROFILE_USE=${writeLlvmProfile(build)}`,
|
||||
);
|
||||
else args.push("-DSCRIPTC_LLVM_THINLTO=OFF", "-DSCRIPTC_LLVM_PROFILE_USE=");
|
||||
if (process.env.SCRIPTC_LLVM_PROFILE_GENERATE === "1")
|
||||
args.push(
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DSCRIPTC_LLVM_PROFILE_GENERATE=ON",
|
||||
);
|
||||
else args.push("-DSCRIPTC_LLVM_PROFILE_GENERATE=OFF");
|
||||
return args;
|
||||
}
|
||||
Binary file not shown.
@@ -173,6 +173,23 @@ void limitProfileImports() {
|
||||
(void)Limited;
|
||||
}
|
||||
|
||||
// Drops the names of a partition's struct types before its summary is
|
||||
// written. Every partition declares the program's types under the same
|
||||
// names, and a thin link loads the importing partition and the partitions
|
||||
// it imports from into one context, which tells same-named types apart by
|
||||
// appending a counter that runs across the whole link. An edit that adds a
|
||||
// type anywhere shifts the counter, so the module after importing, the
|
||||
// object cache key, changed in partitions whose code had not. Unnamed types
|
||||
// carry no such suffix; importing still matches them by their bodies.
|
||||
struct UnnameStructTypes : PassInfoMixin<UnnameStructTypes> {
|
||||
PreservedAnalyses run(Module &M, ModuleAnalysisManager &) {
|
||||
for (StructType *Type : M.getIdentifiedStructTypes())
|
||||
Type->setName("");
|
||||
return PreservedAnalyses::all();
|
||||
}
|
||||
static bool isRequired() { return true; }
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -733,6 +750,7 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
if (Annotating)
|
||||
P.countAnnotated(PartitionMatch.Annotated);
|
||||
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
|
||||
MPM.addPass(UnnameStructTypes());
|
||||
MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false,
|
||||
/*EmitSummaryIndex=*/true,
|
||||
/*EmitModuleHash=*/true));
|
||||
|
||||
@@ -49,6 +49,7 @@ export function relocateToolchain(manifest, sourceDirectory, destinationDirector
|
||||
"declarations",
|
||||
"comptime",
|
||||
"wasi_node_runner",
|
||||
"lib_diagnostics",
|
||||
]) {
|
||||
if (typeof relocated[name] === "string") relocated[name] = pathFrom(relocated[name]);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true as const } : {}),
|
||||
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
|
||||
...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}),
|
||||
...(checker === undefined ? {} : { checker }),
|
||||
npmStatic,
|
||||
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
|
||||
@@ -220,6 +221,7 @@ async function coverageFastPath(
|
||||
values.strip ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values.lib ||
|
||||
values.profile !== undefined ||
|
||||
values.ffi !== undefined ||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
mergeProfiles,
|
||||
resolveProvenanceSources,
|
||||
sourceTargetPlatform,
|
||||
typePatches,
|
||||
warmNativeCaches,
|
||||
} from "@scriptc/compiler";
|
||||
import { runCli } from "./command.js";
|
||||
@@ -39,6 +40,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
||||
sourceTargetPlatform,
|
||||
warmNativeCaches,
|
||||
mergeProfiles,
|
||||
typePatches,
|
||||
run: (binary) =>
|
||||
new Promise<number>((resolveExit) => {
|
||||
let child;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const require = createRequire(import.meta.url);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
|
||||
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
||||
|
||||
const program = `function mix(a: Float64Array, b: Float64Array, k: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) s += a[i]! * k + b[i]! / (i + 1);
|
||||
return s;
|
||||
}
|
||||
const a = new Float64Array(4096);
|
||||
const b = new Float64Array(4096);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
a[i] = ((i * 7919) % 1000) / 7 - 50;
|
||||
b[i] = 1 / (i + 3);
|
||||
}
|
||||
let total = 0;
|
||||
for (let r = 0; r < 32; r++) total += mix(a, b, r * 0.1);
|
||||
console.log(total, [3, 1, 2].map((x) => mix(a.subarray(x), b, x)).join(" "));
|
||||
`;
|
||||
|
||||
test.runIf(process.platform === "linux" && process.arch === "x64")(
|
||||
"--cpu-dispatch builds one executable that matches Node at every level",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join("/tmp", "scriptc-cpu-dispatch-"));
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "program");
|
||||
const llvmPath = join(dir, "main.ll");
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
|
||||
const build = async (args: string[], bootstrap = false): Promise<string> => {
|
||||
await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
|
||||
"build",
|
||||
entry,
|
||||
"-o",
|
||||
outPath,
|
||||
...args,
|
||||
],
|
||||
{ env, maxBuffer: 4 * 1024 * 1024 },
|
||||
);
|
||||
return readFile(llvmPath, "utf8");
|
||||
};
|
||||
const run = async (level?: string): Promise<string> =>
|
||||
(
|
||||
await execFileAsync(outPath, [], {
|
||||
env: { ...process.env, ...(level === undefined ? {} : { SCRIPTC_CPU_LEVEL: level }) },
|
||||
})
|
||||
).stdout;
|
||||
try {
|
||||
await writeFile(entry, program);
|
||||
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
|
||||
const plain = await build(["--optimization=speed"]);
|
||||
expect(plain).not.toContain("@sc_cpu_level");
|
||||
const dispatching = await build(["--optimization=speed", "--cpu-dispatch"]);
|
||||
expect(dispatching).toMatch(/^define internal double @sc_bf_mix\.v3\(/m);
|
||||
for (const level of [undefined, "0", "baseline", "3", "x86-64-v3"])
|
||||
expect(await run(level), `SCRIPTC_CPU_LEVEL=${level}`).toBe(expected);
|
||||
// The bootstrap's cached route keeps the two builds apart.
|
||||
expect(await build(["--optimization=speed"], true)).toBe(plain);
|
||||
expect(await build(["--optimization=speed", "--cpu-dispatch"], true)).toBe(dispatching);
|
||||
expect(await build(["--cpu-dispatch"])).toContain("@sc_cpu_level_init()");
|
||||
expect(await run("0")).toBe(expected);
|
||||
expect(await build(["--target-cpu=x86-64-v3", "--cpu-dispatch"])).not.toContain(
|
||||
"@sc_cpu_level",
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
@@ -53,6 +53,7 @@ function fixture() {
|
||||
dsymutil: "dsymutil",
|
||||
comptime: "../lib/comptime",
|
||||
wasi_node_runner: "../lib/wasi/cli/wasi-runner.js",
|
||||
lib_diagnostics: "../lib/lib-diagnostics.json",
|
||||
};
|
||||
writeFileSync(binary + ".json", JSON.stringify(toolchain));
|
||||
const helpers = [
|
||||
@@ -93,6 +94,9 @@ test("installs a direct executable and relocatable references to platform assets
|
||||
expect(resolve(dirname(installed), manifest.wasi_node_runner)).toBe(
|
||||
join(platform, "wasi/cli/wasi-runner.js"),
|
||||
);
|
||||
expect(resolve(dirname(installed), manifest.lib_diagnostics)).toBe(
|
||||
join(platform, "lib-diagnostics.json"),
|
||||
);
|
||||
expect(resolve(dirname(installed), manifest.runtime_packs[0].path)).toBe(
|
||||
join(platform, "runtime"),
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
|
||||
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
|
||||
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_v45");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v46");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("output option compatibility", () => {
|
||||
[{ emit: "llvm", strip: true }, /strip/],
|
||||
[{ emit: "obj", detachedExit: true }, /detached-exit/],
|
||||
[{ emit: "llvm", detachedExit: true }, /detached-exit/],
|
||||
[{ emit: "asm", cpuDispatch: true }, /cpu-dispatch/],
|
||||
[{ emit: "obj", profileGenerate: "/tmp/%p.profraw" }, /profile-generate/],
|
||||
[{ emit: "llvm", profileUse: "/tmp/a.profdata" }, /profile-use/],
|
||||
[{ profileGenerate: "/tmp/%p.profraw", profileUse: "/tmp/a.profdata" }, /cannot be combined/],
|
||||
|
||||
@@ -31,12 +31,14 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.typeOrigins ? { typeOrigins: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.mime ? { mime: true } : {}),
|
||||
...(features.utilTiming ? { utilTiming: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
...(features.computedRequire ? { computedRequire: true } : {}),
|
||||
events: features.processEvents,
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import { MACOS_ARM64_TARGET, LINUX_ARM64_GNU_TARGET, LINUX_X64_GNU_TARGET } from "./targets.js";
|
||||
|
||||
test("Linux links libm as needed and other system libraries as given", () => {
|
||||
for (const target of [LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET]) {
|
||||
expect(systemLibraryLinkerArgs(target, ["m"])).toEqual([
|
||||
"-Wl,--as-needed",
|
||||
"-lm",
|
||||
"-Wl,--no-as-needed",
|
||||
]);
|
||||
expect(systemLibraryLinkerArgs(target, ["z", "m"])).toEqual([
|
||||
"-lz",
|
||||
"-Wl,--as-needed",
|
||||
"-lm",
|
||||
"-Wl,--no-as-needed",
|
||||
]);
|
||||
}
|
||||
expect(systemLibraryLinkerArgs(MACOS_ARM64_TARGET, ["System"])).toEqual(["-lSystem"]);
|
||||
});
|
||||
@@ -70,3 +70,19 @@ export function executableLinkInputs(options: {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** The linker arguments for an executable's system libraries. On Linux, libm
|
||||
* links as needed: the runtime defines the libm functions most programs
|
||||
* reach (scr_glibc_math.c) and Math's transcendentals (scr_ieee754.c), so a
|
||||
* program that calls no other libm function, such as pow, starts without
|
||||
* loading libm.so. */
|
||||
export function systemLibraryLinkerArgs(
|
||||
target: Pick<NativeTargetSpec, "platform">,
|
||||
names: readonly string[],
|
||||
): string[] {
|
||||
return names.flatMap((name) =>
|
||||
name === "m" && target.platform === "linux"
|
||||
? ["-Wl,--as-needed", "-lm", "-Wl,--no-as-needed"]
|
||||
: [`-l${name}`],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
installDarwinDebugSymbols,
|
||||
@@ -248,7 +249,7 @@ export async function linkNativeExecutable(
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...plan.systemLibraries.map((name) => `-l${name}`),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
|
||||
@@ -101,6 +101,13 @@ export class CheckedNarrows {
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to a checked extraction whose helper borrows its argument, so
|
||||
* emitCheckedNarrowReceiver can lend the payload instead of calling it. */
|
||||
export function borrowsCheckedNarrow(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "call" || host.checkedNarrows.get(e) === null) return false;
|
||||
return host.callLifetimes.borrowed.get(e.callee)?.has(0) === true && host.mayThrow.has(e.callee);
|
||||
}
|
||||
|
||||
/** A checked extraction consumed at once by a field or tag load. The
|
||||
* successful arm borrows the payload from the union, which itself borrows
|
||||
* from its source: a dense array read keeps the element in the array slot,
|
||||
@@ -113,7 +120,7 @@ export function emitCheckedNarrowReceiver(
|
||||
narrow: CheckedNarrowCall,
|
||||
): LlValue | null {
|
||||
const callee = narrow.call.callee;
|
||||
if (!host.callLifetimes.borrowed.get(callee)?.has(0) || !host.mayThrow.has(callee)) return null;
|
||||
if (!borrowsCheckedNarrow(host, narrow.call)) return null;
|
||||
const B = host.B;
|
||||
const read = host.optionalArrayReads.get(narrow.value);
|
||||
const union = read
|
||||
|
||||
@@ -573,13 +573,21 @@ export function classIdLoadLines(
|
||||
* 64-bit word. The count is the i32 at offset 0. */
|
||||
export const CLASS_ID_OFFSET = 4;
|
||||
|
||||
/** The bound on layout ids (scr_rc.h SCR_RC_LAYOUT_ID_LIMIT): the high
|
||||
* half of a 64-bit rc word keeps the layout id in its low 30 bits, beside
|
||||
* the program-lifetime and frozen bits. A class id is the layout id of its
|
||||
* class's canonical layout. */
|
||||
export const LAYOUT_ID_LIMIT = 0x3fffffff;
|
||||
|
||||
/** The rc word an LlClassMeta.idInRc instance is created with: the class
|
||||
* id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits
|
||||
* a count of 1, or for a program-lifetime instance the midpoint of the 32-bit
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is never 0,
|
||||
* 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so
|
||||
* the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is a layout id
|
||||
* (LAYOUT_ID_LIMIT), so it never sets the program-lifetime or frozen bit
|
||||
* and the runtime reads it back (scr_rc.h scr_rc_layout_id). */
|
||||
export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
if (meta.pre + 1 >= LAYOUT_ID_LIMIT)
|
||||
throw new InternalCompilerError(`class id ${meta.pre + 1} is not a layout id`);
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,10 @@ export function emitFieldAbsentTest(B: BlockBuilder, value: string, undefinedTag
|
||||
return absent;
|
||||
}
|
||||
|
||||
/** ScrClosure.function_kind's SCR_FN_RECEIVER_FREE bit (scr_closure.h):
|
||||
* the body never observes the ambient receiver. */
|
||||
export const CLOSURE_RECEIVER_FREE = 16;
|
||||
|
||||
/** The right-hand side of a JS function-identity comparison: two closures
|
||||
* are one function when their identity roots match (a signature adapter
|
||||
* stands for the function it adapts). Both operands must be closures. */
|
||||
|
||||
@@ -299,6 +299,47 @@ test("receiver readers propagate backwards through direct calls and recursion",
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("signature adapters that only forward the receiver ignore it with their original", () => {
|
||||
const forward = (receiver: IrExpr): IrStmt => ({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "callValue",
|
||||
callee: ref("f.0"),
|
||||
receiver,
|
||||
args: [{ kind: "varRef", localId: "a.0", type: F64, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const ambient: IrExpr = { kind: "libCall", fn: "dyn.this", args: [], type: { kind: "dyn" }, loc };
|
||||
const adapter: IrFunction = {
|
||||
name: "%fn.adapt.x.impl",
|
||||
params: [{ localId: "a.0", name: "a", type: F64 }],
|
||||
captures: [{ localId: "f.0", name: "f", type: callback }],
|
||||
locals: [
|
||||
{ id: "f.0", name: "f", type: callback, mutable: false, boxed: true },
|
||||
{ id: "a.0", name: "a", type: F64, mutable: false },
|
||||
],
|
||||
returnType: BOOL,
|
||||
body: [forward(ambient)],
|
||||
loc,
|
||||
};
|
||||
const readers = (): AmbientReceiverReaders =>
|
||||
new AmbientReceiverReaders(new Map([[adapter.name, adapter]]));
|
||||
// The adapter itself reads the receiver, only to forward it.
|
||||
expect(readers().ignores(adapter)).toBe(false);
|
||||
expect(readers().forwardsOnly(adapter)).toBe(true);
|
||||
// Any other read of the receiver, or forwarding something else, does not.
|
||||
adapter.body = [thisRead, forward(ambient)];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ref("a.0", F64))];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ambient)];
|
||||
adapter.captures = [];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
});
|
||||
|
||||
test("virtual calls admit every same-named method", () => {
|
||||
const call: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
/* Runtime CPU dispatch for x86-64 executables (cloneForCpuDispatch): the
|
||||
* program's functions get a copy compiled for x86-64-v3, chosen once per
|
||||
* call by the CPU level the executable detects at startup. */
|
||||
import type { TargetCpu } from "../target-cpu.js";
|
||||
|
||||
/** The levels a dispatching executable carries a copy of its program's
|
||||
* functions for, above the baseline. */
|
||||
export const CPU_DISPATCH_TIERS: readonly TargetCpu[] = ["x86-64-v3"];
|
||||
|
||||
/** The CPU level the executable runs at: 1 (the x86-64 baseline) or 3
|
||||
* (x86-64-v3), set by @sc_cpu_level_init before the program runs. */
|
||||
const LEVEL = "@sc_cpu_level";
|
||||
|
||||
/** The attribute groups of the x86-64-v3 copies: #0's and #2's (see
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`. */
|
||||
const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
|
||||
["0", "3"],
|
||||
["2", "4"],
|
||||
]);
|
||||
|
||||
export function cpuDispatchAttributes(asan: string): string[] {
|
||||
return [
|
||||
`attributes #3 = {${asan} "target-cpu"="x86-64-v3" }`,
|
||||
`attributes #4 = { noinline${asan} "target-cpu"="x86-64-v3" }`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Bits of CPUID leaf 1's ECX that x86-64-v3 needs: SSE3 (0), SSSE3 (9),
|
||||
* FMA (12), CMPXCHG16B (13), SSE4.1 (19), SSE4.2 (20), MOVBE (22), POPCNT
|
||||
* (23), XSAVE (26), OSXSAVE (27), AVX (28) and F16C (29). */
|
||||
const LEAF1_ECX = 0x3cd83201;
|
||||
/** Leaf 7's EBX: BMI1 (3), AVX2 (5) and BMI2 (8). */
|
||||
const LEAF7_EBX = 0x128;
|
||||
/** Leaf 0x80000001's ECX: LAHF/SAHF (0) and LZCNT (5). */
|
||||
const EXT1_ECX = 0x21;
|
||||
|
||||
/** `SCRIPTC_CPU_LEVEL` values and the level each caps the executable at. */
|
||||
const LEVEL_NAMES: readonly [string, number][] = [
|
||||
["0", 0],
|
||||
["1", 1],
|
||||
["2", 2],
|
||||
["3", 3],
|
||||
["4", 4],
|
||||
["baseline", 1],
|
||||
["v1", 1],
|
||||
["v2", 2],
|
||||
["v3", 3],
|
||||
["v4", 4],
|
||||
["x86-64", 1],
|
||||
["x86-64-v2", 2],
|
||||
["x86-64-v3", 3],
|
||||
["x86-64-v4", 4],
|
||||
];
|
||||
|
||||
/** @sc_cpu_level and @sc_cpu_level_init, which main calls first:
|
||||
* - CPUID reports every x86-64-v3 feature, and XGETBV that the operating
|
||||
* system saves the SSE and AVX registers, for level 3; otherwise 1;
|
||||
* - `SCRIPTC_CPU_LEVEL` (a level from 0 to 4, `baseline` or an `x86-64-vN`
|
||||
* name) caps the level; another value caps it at 0. */
|
||||
export function cpuLevelDefs(declare: (line: string) => void): string[] {
|
||||
declare(`declare ptr @getenv(ptr)`);
|
||||
declare(`declare i32 @strcmp(ptr, ptr)`);
|
||||
const bytes = (text: string): string => `[${text.length + 1} x i8] c"${text}\\00"`;
|
||||
const lines = [
|
||||
`${LEVEL} = internal global i32 1`,
|
||||
`@sc_cpu_level_env = private unnamed_addr constant ${bytes("SCRIPTC_CPU_LEVEL")}`,
|
||||
...LEVEL_NAMES.map(
|
||||
([name], i) => `@sc_cpu_level_name_${i} = private unnamed_addr constant ${bytes(name)}`,
|
||||
),
|
||||
`define internal void @sc_cpu_level_init() #0 {`,
|
||||
`entry:`,
|
||||
` %max = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 0, i32 0)`,
|
||||
` %max.leaf = extractvalue { i32, i32, i32, i32 } %max, 0`,
|
||||
` %has.leaf7 = icmp uge i32 %max.leaf, 7`,
|
||||
` br i1 %has.leaf7, label %leaf1, label %detected`,
|
||||
`leaf1:`,
|
||||
` %l1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 1, i32 0)`,
|
||||
` %l1.ecx = extractvalue { i32, i32, i32, i32 } %l1, 2`,
|
||||
` %l1.bits = and i32 %l1.ecx, ${LEAF1_ECX}`,
|
||||
` %l1.ok = icmp eq i32 %l1.bits, ${LEAF1_ECX}`,
|
||||
` br i1 %l1.ok, label %leaf7, label %detected`,
|
||||
`leaf7:`,
|
||||
` %l7 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 7, i32 0)`,
|
||||
` %l7.ebx = extractvalue { i32, i32, i32, i32 } %l7, 1`,
|
||||
` %l7.bits = and i32 %l7.ebx, ${LEAF7_EBX}`,
|
||||
` %l7.ok = icmp eq i32 %l7.bits, ${LEAF7_EBX}`,
|
||||
` %ext = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483648, i32 0)`,
|
||||
` %ext.max = extractvalue { i32, i32, i32, i32 } %ext, 0`,
|
||||
` %ext.has = icmp uge i32 %ext.max, -2147483647`,
|
||||
` %l7.ext = and i1 %l7.ok, %ext.has`,
|
||||
` br i1 %l7.ext, label %ext1, label %detected`,
|
||||
`ext1:`,
|
||||
` %e1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483647, i32 0)`,
|
||||
` %e1.ecx = extractvalue { i32, i32, i32, i32 } %e1, 2`,
|
||||
` %e1.bits = and i32 %e1.ecx, ${EXT1_ECX}`,
|
||||
` %e1.ok = icmp eq i32 %e1.bits, ${EXT1_ECX}`,
|
||||
` br i1 %e1.ok, label %xcr0, label %detected`,
|
||||
// Leaf 1 reported OSXSAVE, so XGETBV is available.
|
||||
`xcr0:`,
|
||||
` %xcr = call { i32, i32 } asm "xgetbv", "={ax},={dx},{cx}"(i32 0)`,
|
||||
` %xcr.lo = extractvalue { i32, i32 } %xcr, 0`,
|
||||
` %xcr.bits = and i32 %xcr.lo, 6`,
|
||||
` %xcr.ok = icmp eq i32 %xcr.bits, 6`,
|
||||
` %v3 = select i1 %xcr.ok, i32 3, i32 1`,
|
||||
` br label %detected`,
|
||||
`detected:`,
|
||||
` %level = phi i32 [ 1, %entry ], [ 1, %leaf1 ], [ 1, %leaf7 ], [ 1, %ext1 ], [ %v3, %xcr0 ]`,
|
||||
` %env = call ptr @getenv(ptr @sc_cpu_level_env)`,
|
||||
` %has.env = icmp ne ptr %env, null`,
|
||||
` br i1 %has.env, label %cap, label %store`,
|
||||
`cap:`,
|
||||
];
|
||||
// The matching name's level, or 0 when none matches.
|
||||
let cap = "0";
|
||||
LEVEL_NAMES.forEach(([, level], i) => {
|
||||
lines.push(
|
||||
` %name.${i} = call i32 @strcmp(ptr %env, ptr @sc_cpu_level_name_${i})`,
|
||||
` %is.${i} = icmp eq i32 %name.${i}, 0`,
|
||||
` %cap.${i} = select i1 %is.${i}, i32 ${level}, i32 ${cap}`,
|
||||
);
|
||||
cap = `%cap.${i}`;
|
||||
});
|
||||
lines.push(
|
||||
` %capped.below = icmp ult i32 ${cap}, %level`,
|
||||
` %capped = select i1 %capped.below, i32 ${cap}, i32 %level`,
|
||||
` br label %store`,
|
||||
`store:`,
|
||||
` %final = phi i32 [ %level, %detected ], [ %capped, %cap ]`,
|
||||
` store i32 %final, ptr ${LEVEL}`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The call main makes before anything else. */
|
||||
export const CPU_LEVEL_INIT_CALL = ` call void @sc_cpu_level_init()`;
|
||||
|
||||
/** A function definition's header, split around its name and parameters. */
|
||||
interface Header {
|
||||
/** `define` and the linkage, up to the return type. */
|
||||
linkage: string;
|
||||
/** The return type with its attributes. */
|
||||
ret: string;
|
||||
name: string;
|
||||
/** The parameter list between the parentheses. */
|
||||
params: string;
|
||||
/** Function attributes before the group (`noinline`, `inlinehint`). */
|
||||
attrs: string;
|
||||
group: string;
|
||||
/** What follows the group: a personality, then `{` and a comment. */
|
||||
rest: string;
|
||||
}
|
||||
|
||||
const HEADER_START = /^define ((?:internal |private )?)(.*?)@([\w.$-]+)\(/;
|
||||
const HEADER_END = /^((?: [a-z_]+)*) #(\d+)( .*)$/;
|
||||
const RETURN_ATTRIBUTES =
|
||||
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
|
||||
|
||||
function parseHeader(line: string): Header | null {
|
||||
const start = HEADER_START.exec(line);
|
||||
if (start === null) return null;
|
||||
const open = start[0].length - 1;
|
||||
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;
|
||||
}
|
||||
const end = HEADER_END.exec(line.slice(close + 1));
|
||||
if (end === null) return null;
|
||||
return {
|
||||
linkage: `define ${start[1]!}`,
|
||||
ret: start[2]!.trim(),
|
||||
name: start[3]!,
|
||||
params: line.slice(open + 1, close),
|
||||
attrs: end[1]!,
|
||||
group: end[2]!,
|
||||
rest: end[3]!,
|
||||
};
|
||||
}
|
||||
|
||||
/** The parameters split at their top-level commas. */
|
||||
function splitParams(params: string): string[] {
|
||||
const out: string[] = [];
|
||||
let depth = 0;
|
||||
let from = 0;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const c = params.charAt(i);
|
||||
if (c === "(" || c === "{" || c === "[" || c === "<") depth++;
|
||||
else if (c === ")" || c === "}" || c === "]" || c === ">") depth--;
|
||||
else if (c === "," && depth === 0) {
|
||||
out.push(params.slice(from, i).trim());
|
||||
from = i + 1;
|
||||
}
|
||||
}
|
||||
if (params.trim() !== "") out.push(params.slice(from).trim());
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A definition the pass copies: one in an attribute group it has a copy
|
||||
* group for (coroutine bodies, #1, keep their one body), with named
|
||||
* parameters, no variable arguments and no debug locations. */
|
||||
function cloneable(header: Header, text: string): boolean {
|
||||
if (!COPY_GROUPS.has(header.group)) return false;
|
||||
if (text.includes("!dbg")) return false;
|
||||
return splitParams(header.params).every((param) => /%[\w.$-]+$/.test(param));
|
||||
}
|
||||
|
||||
/** The dispatching definition under the function's own name: it tail-calls
|
||||
* the x86-64-v3 copy at level 3 or higher and the baseline copy otherwise. */
|
||||
function dispatcher(header: Header): string {
|
||||
const args = splitParams(header.params).join(", ");
|
||||
const type = header.ret.replace(RETURN_ATTRIBUTES, "").trim();
|
||||
const call = (tier: string, result: string): string[] =>
|
||||
type === "void"
|
||||
? [` musttail call void @${header.name}.${tier}(${args})`, ` ret void`]
|
||||
: [
|
||||
` ${result} = musttail call ${header.ret} @${header.name}.${tier}(${args})`,
|
||||
` ret ${type} ${result}`,
|
||||
];
|
||||
return [
|
||||
`${header.linkage}${header.ret} @${header.name}(${header.params}) #0 {`,
|
||||
`entry:`,
|
||||
` %sc.cpu.level = load i32, ptr ${LEVEL}`,
|
||||
` %sc.cpu.v3 = icmp uge i32 %sc.cpu.level, 3`,
|
||||
` br i1 %sc.cpu.v3, label %sc.cpu.v3.call, label %sc.cpu.base.call`,
|
||||
`sc.cpu.v3.call:`,
|
||||
...call("v3", "%sc.cpu.v3.result"),
|
||||
`sc.cpu.base.call:`,
|
||||
...call("base", "%sc.cpu.base.result"),
|
||||
`}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** A definition split at its header line and its closing brace. */
|
||||
interface Definition {
|
||||
header: Header;
|
||||
/** The lines after the header, through the closing `}`. */
|
||||
body: string;
|
||||
/** Text after the closing brace, kept as it is. */
|
||||
after: string;
|
||||
}
|
||||
|
||||
/** Rewrites the program's function definitions in `parts` for runtime
|
||||
* dispatch. A definition is a part, or a `define` chunk of a part, that
|
||||
* follows a partition group line (emit-module.ts partitionGroupLines);
|
||||
* runtime helpers and the module's entry points stay as they are. Each
|
||||
* cloneable function `@f` becomes:
|
||||
* - `@f.base`, its body as before;
|
||||
* - `@f.v3`, the same body in an x86-64-v3 attribute group;
|
||||
* - `@f`, a dispatcher with the same signature, which every reference to
|
||||
* the function as a value and every call from outside the program's
|
||||
* functions still names; a function with no such reference gets none.
|
||||
* Direct calls between cloned functions go to the caller's own tier, so the
|
||||
* level is tested once on entry into the program's code. */
|
||||
export function cloneForCpuDispatch(parts: readonly string[]): string[] {
|
||||
const program: { index: number; chunks: string[] }[] = [];
|
||||
for (let i = 1; i < parts.length; i++)
|
||||
if (parts[i - 1]!.startsWith("; scriptc-group") && parts[i]!.startsWith("define "))
|
||||
program.push({ index: i, chunks: parts[i]!.split(/\n(?=define )/) });
|
||||
const defs = new Map<string, Definition>();
|
||||
for (const { chunks } of program)
|
||||
for (const chunk of chunks) {
|
||||
const newline = chunk.indexOf("\n");
|
||||
const header = newline < 0 ? null : parseHeader(chunk.slice(0, newline));
|
||||
if (header === null) continue;
|
||||
const close = chunk.indexOf("\n}", newline);
|
||||
if (close < 0) continue;
|
||||
const body = chunk.slice(newline + 1, close + 2);
|
||||
if (!cloneable(header, body)) continue;
|
||||
defs.set(header.name, { header, body, after: chunk.slice(close + 2) });
|
||||
}
|
||||
if (defs.size === 0) return [...parts];
|
||||
const callee = /@([\w.$-]+)\(/g;
|
||||
const retarget = (body: string, tier: string): string =>
|
||||
body.replace(callee, (all, name: string) => (defs.has(name) ? `@${name}.${tier}(` : all));
|
||||
const named = namedFunctions(parts, program, defs, (body) => retarget(body, "base"));
|
||||
const out = [...parts];
|
||||
for (const { index, chunks } of program)
|
||||
out[index] = chunks
|
||||
.map((chunk) => {
|
||||
const header = HEADER_START.exec(chunk);
|
||||
const def = header === null ? undefined : defs.get(header[3]!);
|
||||
if (def === undefined) return chunk;
|
||||
const h = def.header;
|
||||
const copy = (tier: string, group: string): string =>
|
||||
`define internal ${h.ret} @${h.name}.${tier}(${h.params})${h.attrs} #${group}${h.rest}\n${retarget(def.body, tier)}`;
|
||||
return (
|
||||
[
|
||||
copy("base", h.group),
|
||||
copy("v3", COPY_GROUPS.get(h.group)!),
|
||||
...(named.has(h.name) ? [dispatcher(h)] : []),
|
||||
].join("\n") + def.after
|
||||
);
|
||||
})
|
||||
.join("\n");
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The cloned functions whose own name the module still uses once the
|
||||
* copies call each other: as a value, or in a call from code that is not
|
||||
* copied (main, runtime helpers, coroutine bodies). Only these get a
|
||||
* dispatcher. A function without one keeps the callers it had, so LLVM
|
||||
* inlines and removes its copies as it would the original. */
|
||||
function namedFunctions(
|
||||
parts: readonly string[],
|
||||
program: readonly { index: number; chunks: string[] }[],
|
||||
defs: ReadonlyMap<string, Definition>,
|
||||
retargeted: (body: string) => string,
|
||||
): Set<string> {
|
||||
const named = new Set<string>();
|
||||
const scan = (text: string): void => {
|
||||
for (const m of text.matchAll(/@([\w.$-]+)/g)) if (defs.has(m[1]!)) named.add(m[1]!);
|
||||
};
|
||||
const inProgram = new Set(program.map(({ index }) => index));
|
||||
parts.forEach((part, i) => {
|
||||
if (!inProgram.has(i)) scan(part);
|
||||
});
|
||||
for (const { chunks } of program)
|
||||
for (const chunk of chunks) {
|
||||
const header = HEADER_START.exec(chunk);
|
||||
const def = header === null ? undefined : defs.get(header[3]!);
|
||||
if (def === undefined) scan(chunk);
|
||||
else scan(retargeted(def.body) + def.after);
|
||||
}
|
||||
return named;
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import { CPU_LEVEL_INIT_CALL, cpuDispatchAttributes, cpuLevelDefs } from "./cpu-dispatch.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
@@ -198,7 +199,8 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
|
||||
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
|
||||
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
|
||||
* builds. A group needs one attribute: without either, #0 holds a string
|
||||
* attribute that LLVM ignores. */
|
||||
* attribute that LLVM ignores. Dispatching builds add the x86-64-v3 copies'
|
||||
* #3 and #4 (cpu-dispatch.ts). */
|
||||
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
|
||||
const cpu = targetCpuAttribute(emitter.targetCpu);
|
||||
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
|
||||
@@ -207,6 +209,7 @@ function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean)
|
||||
`attributes #0 = {${base} }`,
|
||||
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
|
||||
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
|
||||
...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -585,6 +588,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
...(emitter.publishWalkersCache?.defs() ?? []),
|
||||
...emitter.publishedClassNamesDefs(),
|
||||
...(emitter.regions?.moduleDefs() ?? []),
|
||||
...(emitter.cpuDispatch ? cpuLevelDefs((line) => emitter.declare(line)) : []),
|
||||
];
|
||||
if (emitter.constantNumericTables.size > 0)
|
||||
emitter.declare(`declare double @scr_arr_get_number(ptr, double)`);
|
||||
@@ -1043,6 +1047,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
`define i32 @${emitter.mod.workers ? "sc_context_entry" : emitter.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
|
||||
...(emitter.cpuDispatch && !emitter.mod.workers ? [CPU_LEVEL_INIT_CALL] : []),
|
||||
...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []),
|
||||
` call void @scr_init()`,
|
||||
// Compressed fields hold addresses in the program-lifetime region, which
|
||||
@@ -1201,6 +1206,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
`}`,
|
||||
`define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
// Worker threads enter at sc_worker_entry and read the level main set.
|
||||
...(emitter.cpuDispatch ? [CPU_LEVEL_INIT_CALL] : []),
|
||||
` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
|
||||
` call void @scr_context_cleanup()`,
|
||||
` ret i32 %result`,
|
||||
|
||||
@@ -147,6 +147,7 @@ import {
|
||||
bridgePlainUnwindCalls,
|
||||
unwindAbiFunctions,
|
||||
} from "./unwind-abi.js";
|
||||
import { cloneForCpuDispatch } from "./cpu-dispatch.js";
|
||||
import {
|
||||
mangleAsyncInlineAwaitCall,
|
||||
mangleAsyncSpawn,
|
||||
@@ -183,14 +184,14 @@ import {
|
||||
unionBoxBits,
|
||||
} from "./inline-unions.js";
|
||||
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
|
||||
import { CheckedNarrows } from "./checked-narrows.js";
|
||||
import { borrowsCheckedNarrow, CheckedNarrows } from "./checked-narrows.js";
|
||||
import {
|
||||
emitDenseArraySet,
|
||||
emitDenseSlotUnlink,
|
||||
SCR_ELEM_I32,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { matchMapRead, pointerMapGet, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
@@ -213,7 +214,13 @@ import {
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, SCR_ARR_FROZEN } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
emitFieldAbsentTest,
|
||||
f64Lit,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_FROZEN,
|
||||
} from "./common.js";
|
||||
import { ffiExtendsNarrowIntegers } from "../targets.js";
|
||||
import {
|
||||
emitLiteralExpr,
|
||||
@@ -285,7 +292,12 @@ import {
|
||||
emitBytesSet,
|
||||
emitBytesIntrinsic,
|
||||
} from "./expr-bytes.js";
|
||||
import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
|
||||
import {
|
||||
emitRecordKeyGet,
|
||||
emitRegexIntrinsic,
|
||||
keyedRecordReadInto,
|
||||
pointerRecordKeyGet,
|
||||
} from "./expr-records.js";
|
||||
import {
|
||||
dynPromiseAdapter,
|
||||
streamTypedRefCommitAdapter,
|
||||
@@ -533,6 +545,11 @@ export interface LlvmTargetOptions {
|
||||
* every emitted function carries it as its `target-cpu`. Unset, code
|
||||
* runs on any x86-64 CPU. */
|
||||
targetCpu?: TargetCpu;
|
||||
/** Give an x86-64 executable without a `targetCpu` an x86-64-v3 copy of
|
||||
* each program function, which it runs on CPUs at that level
|
||||
* (cpu-dispatch.ts). Ignored for other targets, libraries and builds
|
||||
* with debug information. */
|
||||
cpuDispatch?: boolean;
|
||||
}
|
||||
|
||||
/** LlvmTargetOptions.lowRegion for an executable: Linux targets without
|
||||
@@ -1277,6 +1294,8 @@ export class LlEmitter {
|
||||
readonly targetCpu: TargetCpu | undefined;
|
||||
/** Emitted functions carry `sanitize_address` (LlvmTargetOptions). */
|
||||
readonly sanitizeAttributes: boolean;
|
||||
/** LlvmTargetOptions.cpuDispatch, where it applies. */
|
||||
readonly cpuDispatch: boolean;
|
||||
readonly wasmBoundsChecks: boolean;
|
||||
/** Interned string literals: UTF-8 text → { symbol, byte length } —
|
||||
* first-use order, the runtime ABI’s determinism discipline. */
|
||||
@@ -1980,6 +1999,13 @@ export class LlEmitter {
|
||||
this.detachedExit = options.detachedExit === true;
|
||||
this.targetCpu = options.targetCpu;
|
||||
this.sanitizeAttributes = options.sanitizeAttributes !== false;
|
||||
this.cpuDispatch =
|
||||
options.cpuDispatch === true &&
|
||||
options.targetCpu === undefined &&
|
||||
!this.wasi &&
|
||||
mod.lib === undefined &&
|
||||
this.debug === null &&
|
||||
/^(x86_64|amd64|x64)\b/.test(options.targetTriple || process.arch);
|
||||
this.wasmBoundsChecks =
|
||||
options.wasmBoundsChecks === true ||
|
||||
wasmBoundsChecks({
|
||||
@@ -2486,7 +2512,8 @@ export class LlEmitter {
|
||||
}
|
||||
emitParts(): string[] {
|
||||
const parts = emitParts(this);
|
||||
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
const bridged = this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
return this.cpuDispatch ? cloneForCpuDispatch(bridged) : bridged;
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
@@ -2560,7 +2587,7 @@ export class LlEmitter {
|
||||
ret === "void" ? ` ${call}` : ` %r = ${call}`,
|
||||
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
|
||||
`}`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${this.closureFunctionKind(fn)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
``,
|
||||
);
|
||||
if (lending)
|
||||
@@ -3112,12 +3139,13 @@ export class LlEmitter {
|
||||
|
||||
// ── plumbing (frame and scope ownership) ──────
|
||||
|
||||
/** A symbol derived from the constant's text rather than its first-use
|
||||
* index, so an edit that interns one more string leaves every other
|
||||
* constant's symbol, and the code referencing it, unchanged. This lets
|
||||
* native builds reuse the compiled partitions an edit does not touch. A
|
||||
* collision retries with a counter, still in first-use order. */
|
||||
private contentSymbol(prefix: string, text: string): string {
|
||||
/** A symbol derived from a constant's text, or a helper's key, rather
|
||||
* than its first-use index, so an edit that interns one more string or
|
||||
* helper leaves every other symbol, and the code referencing it,
|
||||
* unchanged. This lets native builds reuse the compiled partitions an
|
||||
* edit does not touch. A collision retries with a counter, still in
|
||||
* first-use order. */
|
||||
contentSymbol(prefix: string, text: string): string {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const digest = createHash("sha256")
|
||||
.update(attempt === 0 ? text : `${attempt}\0${text}`)
|
||||
@@ -3243,7 +3271,7 @@ export class LlEmitter {
|
||||
const key = `${unionId}:${tag}`;
|
||||
let sym = this.unitInstances.get(key);
|
||||
if (!sym) {
|
||||
sym = `sc_unit_${this.unitInstances.size}`;
|
||||
sym = this.contentSymbol("sc_unit_", key);
|
||||
this.unitInstances.set(key, sym);
|
||||
}
|
||||
this.journal?.entry("unit", key, sym);
|
||||
@@ -3700,6 +3728,9 @@ export class LlEmitter {
|
||||
passThrough?: { name: string; type: IrType },
|
||||
poll = false,
|
||||
guard?: string,
|
||||
/** Runs at the start of the cold unwind block, before the unwind (a
|
||||
* package-declared check's origin note). */
|
||||
onUnwind?: () => void,
|
||||
): void {
|
||||
const through =
|
||||
passThrough !== undefined &&
|
||||
@@ -3724,6 +3755,7 @@ export class LlEmitter {
|
||||
const lk = B.newLabel("exc.k");
|
||||
B.condBr(p, lu, lk);
|
||||
B.startBlock(lu);
|
||||
onUnwind?.();
|
||||
this.emitUnwind(through);
|
||||
B.startBlock(lk);
|
||||
if (skip !== undefined) {
|
||||
@@ -3800,6 +3832,7 @@ export class LlEmitter {
|
||||
B.condBr(cold, lu, lk);
|
||||
}
|
||||
B.startBlock(lu);
|
||||
onUnwind?.();
|
||||
this.emitUnwind(through);
|
||||
B.startBlock(lk);
|
||||
if (skip !== undefined) {
|
||||
@@ -5449,8 +5482,15 @@ export class LlEmitter {
|
||||
case "retainWrapper":
|
||||
addEntry(this.nullableUnions.retainWrappers, key, value as string);
|
||||
return;
|
||||
case "unit":
|
||||
return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names);
|
||||
case "unit": {
|
||||
let own = this.unitInstances.get(key);
|
||||
if (own === undefined) {
|
||||
own = this.contentSymbol("sc_unit_", key);
|
||||
this.unitInstances.set(key, own);
|
||||
}
|
||||
if (own !== value) names.set(value as string, own);
|
||||
return;
|
||||
}
|
||||
case "absent":
|
||||
return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names);
|
||||
case "retype":
|
||||
@@ -5458,9 +5498,9 @@ export class LlEmitter {
|
||||
case "slots":
|
||||
return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names);
|
||||
case "regex":
|
||||
return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names);
|
||||
return this.addContentEntry(this.regexInstances, key, value as never, "sc_re_", names);
|
||||
case "regexReplacer":
|
||||
return addNumberedSymbol(this.regexReplacers, key, value as never, "sc_rr_", names);
|
||||
return this.addContentEntry(this.regexReplacers, key, value as never, "sc_rr_", names);
|
||||
case "templateStrings":
|
||||
return addNumberedSymbol(
|
||||
this.templateStringsInstances,
|
||||
@@ -5737,7 +5777,10 @@ export class LlEmitter {
|
||||
unwrittenRead(e: IrExpr, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
|
||||
// A checked extraction reads nothing besides the union it narrows.
|
||||
const keys = rw.readKeys(e, (call) =>
|
||||
borrowsCheckedNarrow(this, call) ? [] : this.borrowedReturnKeys(call.callee),
|
||||
);
|
||||
return keys !== null && !rw.conflicts(keys, writes);
|
||||
}
|
||||
|
||||
@@ -6014,7 +6057,21 @@ export class LlEmitter {
|
||||
}
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
case "recordKeyGet":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
|
||||
case "mapIntrinsic": {
|
||||
// A Map binding's own pointer (maps are not plainReference types).
|
||||
if (!this.borrowsWithoutOwning(e) || e.receiver.kind !== "varRef") return false;
|
||||
const binding = this.binding(e.receiver.localId);
|
||||
return (
|
||||
binding.kind === "global" ||
|
||||
(binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz &&
|
||||
!this.localUnionStorage.has(e.receiver.localId))
|
||||
);
|
||||
}
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
@@ -7272,6 +7329,11 @@ export class LlEmitter {
|
||||
return this.nullableFieldGet(e) === null;
|
||||
case "recordGet":
|
||||
return true;
|
||||
// A keyed read whose hits are all the result's own pointer.
|
||||
case "recordKeyGet":
|
||||
return pointerRecordKeyGet(this, e);
|
||||
case "mapIntrinsic":
|
||||
return pointerMapGet(this, e);
|
||||
// emitReadReceiver lends a required element of a borrowed array when
|
||||
// computing the index removes no edge.
|
||||
case "arrayGet":
|
||||
@@ -7417,8 +7479,20 @@ export class LlEmitter {
|
||||
return wrapEmitterListener(this, target, adapterFn);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, funcTag);
|
||||
/** ScrClosure.function_kind for a closure over `fn`: generator 1, async
|
||||
* 2, own prototype 4, and SCR_FN_RECEIVER_FREE (16) when its body never
|
||||
* observes the ambient receiver, so indirect callers skip the bracket. */
|
||||
closureFunctionKind(fn: IrFunction): number {
|
||||
return (
|
||||
(fn.generator ? 1 : 0) +
|
||||
(fn.async ? 2 : 0) +
|
||||
(fn.ownsPrototype ? 4 : 0) +
|
||||
(this.receiverReaders.ignores(fn) ? CLOSURE_RECEIVER_FREE : 0)
|
||||
);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, unionId: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, unionId, funcTag);
|
||||
}
|
||||
|
||||
closeBindThunkFor(cbUnion: IrType, retServer: boolean): string {
|
||||
|
||||
@@ -372,6 +372,21 @@ export function emitSerializationExpr(
|
||||
// the dummy harmlessly.
|
||||
// `JSON.parse(text) as T` fuses into one schema-directed parse that
|
||||
// falls back to exactly this route whenever the fast path declines.
|
||||
// A package-declared target: the cold path names the declaration in
|
||||
// the pending TypeError (scr_type_origin.c).
|
||||
// Library archives and reactors link a fixed runtime set without the
|
||||
// note unit: their checks keep the plain message.
|
||||
const origin =
|
||||
e.origin === undefined || host.mod.lib !== undefined
|
||||
? undefined
|
||||
: host.mod.declOrigins?.[e.origin];
|
||||
const note =
|
||||
origin === undefined
|
||||
? undefined
|
||||
: (): void => {
|
||||
host.declare("declare void @scr_type_origin_note(ptr)");
|
||||
B.line(`call void @scr_type_origin_note(ptr ${host.cstr(origin)})`);
|
||||
};
|
||||
const parsed = e.value;
|
||||
if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") {
|
||||
const fused = host.dyn.jsonParseHelper(e.type);
|
||||
@@ -380,7 +395,7 @@ export function emitSerializationExpr(
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${fused}(ptr ${text.name})`);
|
||||
const fusedOut = host.own({ name: r, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
return fusedOut;
|
||||
}
|
||||
}
|
||||
@@ -394,14 +409,14 @@ export function emitSerializationExpr(
|
||||
}
|
||||
const dynV = emitBorrowedInput(host, e.value);
|
||||
const helper = host.dyn.dynCheckHelper(e.type, e.preserveRefs);
|
||||
if (isDynTypedRefType(e.type)) return emitClassCheck(host, e.type, dynV.name, helper);
|
||||
if (isDynTypedRefType(e.type)) return emitClassCheck(host, e.type, dynV.name, helper, note);
|
||||
const ty = host.llType(e.type);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
return out;
|
||||
}
|
||||
default: {
|
||||
@@ -420,6 +435,7 @@ function emitClassCheck(
|
||||
type: IrType & { kind: "object" },
|
||||
value: string,
|
||||
helper: string,
|
||||
note?: () => void,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const slot = B.slot();
|
||||
@@ -460,7 +476,7 @@ function emitClassCheck(
|
||||
B.line(`${result} = call ptr @${helper}(ptr ${value}, ptr null)`);
|
||||
B.line(`store ptr ${result}, ptr ${slot}`);
|
||||
// A throwing check returns null, so the unwind has nothing to release.
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
|
||||
@@ -303,7 +303,7 @@ function emitScopedBytesGet(
|
||||
const signed = elem === "i8" || elem === "i16" || elem === "i32";
|
||||
B.line(`${number} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`);
|
||||
B.line(`${out} = select i1 ${valid}, double ${number}, double ${f64Lit(NaN)}`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
return { name: out, type: F64, uint32: u32, ordered: valid };
|
||||
}
|
||||
|
||||
function emitBytesLoad(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
@@ -54,7 +54,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
const key = `race:${typeKey(from)}=>${typeKey(to)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_race_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_race_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "raceAdapterFor", [from, to]);
|
||||
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
|
||||
@@ -217,7 +217,7 @@ export function genResultThunkFor(
|
||||
const key = `gr:${typeKey(genT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = mangleGenResThunk(host.resolveThunks.size);
|
||||
sym = host.contentSymbol("sc_gres_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]);
|
||||
const shape = host.recordsById.get(recT.shapeId);
|
||||
@@ -474,7 +474,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
const key = `cx:${param.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cx_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
@@ -519,7 +519,7 @@ export function childExitSignalThunkFor(
|
||||
const key = `cx2:${codeParam.unionId}+${sigParam.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cx_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]);
|
||||
const codeDef = host.unionsById.get(codeParam.unionId);
|
||||
@@ -580,7 +580,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `exec:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_exec_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_exec_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "execFileThunkFor", [cbT]);
|
||||
const errorParam = cbT.params[0];
|
||||
@@ -652,7 +652,7 @@ export function ipcMessageThunkFor(
|
||||
const key = `ipcmsg:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_msg_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ipc_msg_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
@@ -703,7 +703,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `ipcsend:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_send_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ipc_send_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
@@ -764,7 +764,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
const key = `cd:${param.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
@@ -799,7 +799,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
|
||||
const key = `cdd:${chunk}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cdd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cdd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]);
|
||||
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
|
||||
@@ -834,7 +834,7 @@ export function emitterFixedAdapter(
|
||||
const key = `ee:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_ad_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_ad_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -905,7 +905,7 @@ export function emitterOnceAdapter(
|
||||
: `eeOnce:${fire}:${typeKey(boxerT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_once_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_once_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -974,10 +974,20 @@ export function wrapEmitterListener(
|
||||
export function unwrapNullableClosure(
|
||||
host: LlvmEmitterContext,
|
||||
u: string,
|
||||
unionId: string,
|
||||
funcTag: number,
|
||||
): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
const nullable = host.nullableUnions.get(unionId);
|
||||
if (nullable !== null) {
|
||||
// The union is the closure pointer itself (NULL for the unit arm).
|
||||
if (nullable.repr !== "single" || nullable.nullTag >= 0 || nullable.refTag !== funcTag)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullable callback union shape");
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr ${retainSym(host.shapeHost, { kind: "union", unionId })}(ptr ${u})`);
|
||||
return r;
|
||||
}
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${slot}`);
|
||||
@@ -1015,7 +1025,7 @@ export function closeBindThunkFor(
|
||||
const key = `ncb:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncb_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ncb_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
@@ -1026,9 +1036,9 @@ export function closeBindThunkFor(
|
||||
"llvm emitter bug: bound-close callback union lacks its func arm",
|
||||
);
|
||||
const oneParam = funcArm.params.length === 1;
|
||||
const cbNullable = host.nullableUnions.get(cbUnion.unionId);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_net_server_close_direct(ptr, ptr)`);
|
||||
let trampoline: string | null = null;
|
||||
@@ -1072,13 +1082,11 @@ export function closeBindThunkFor(
|
||||
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
|
||||
` %bx = load ptr, ptr %capsp`,
|
||||
` %srv = call ptr @scr_box_get_ref(ptr %bx) ; +1`,
|
||||
` %tagp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 1`,
|
||||
` %tag = load i32, ptr %tagp`,
|
||||
...unionTagLines(cbNullable, "%cbu", "%tag"),
|
||||
` %isfn = icmp eq i32 %tag, ${funcTag}`,
|
||||
` br i1 %isfn, label %fn, label %go`,
|
||||
`fn:`,
|
||||
` %pp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 5`,
|
||||
` %pv = load ptr, ptr %pp`,
|
||||
...unionPeekLines(cbNullable, "%cbu", "%pv"),
|
||||
];
|
||||
if (oneParam) {
|
||||
d.push(
|
||||
@@ -1097,7 +1105,7 @@ export function closeBindThunkFor(
|
||||
` br label %go`,
|
||||
`go:`,
|
||||
` %regv = load ptr, ptr %regslot`,
|
||||
` call void @scr_union_release(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void ${releaseSym(host.shapeHost, cbUnion)}(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void @scr_net_server_close_direct(ptr %srv, ptr %regv) ; reg moves`,
|
||||
);
|
||||
if (retServer) {
|
||||
@@ -1121,7 +1129,7 @@ export function closeOverrideWrapFor(
|
||||
const key = `ncw:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncw_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ncw_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
@@ -1169,7 +1177,7 @@ export function settledStoreFor(
|
||||
const key = `settled:${typeKey(from)}=>${typeKey(to)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_settled_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_settled_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "settledStoreFor", [from, to]);
|
||||
const def = host.unionsById.get(to.unionId);
|
||||
|
||||
@@ -42,7 +42,13 @@ import {
|
||||
} from "./classes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
f64Lit,
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
ffiNativeReturnLl,
|
||||
} from "./common.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
@@ -761,8 +767,7 @@ export function emitCallExpr(
|
||||
`${c} = call ptr @scr_closure_new(ptr @${host.callTarget(e.fnName)}, ${host.sizeType} ${e.captures.length})`,
|
||||
);
|
||||
const out = host.own({ name: c, type: e.type });
|
||||
const functionKind =
|
||||
(target.generator ? 1 : 0) + (target.async ? 2 : 0) + (target.ownsPrototype ? 4 : 0);
|
||||
const functionKind = host.closureFunctionKind(target);
|
||||
if (functionKind) {
|
||||
const kindPtr = B.tmp();
|
||||
B.line(`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
@@ -800,6 +805,25 @@ export function emitCallExpr(
|
||||
: 0;
|
||||
host.declare(`declare void @scr_closure_adopt_identity(ptr, ptr, i32)`);
|
||||
B.line(`call void @scr_closure_adopt_identity(ptr ${c}, ptr ${original}, i32 ${length})`);
|
||||
if (host.receiverReaders.forwardsOnly(target)) {
|
||||
// The adapter forwards the ambient receiver to its original,
|
||||
// so it ignores the receiver exactly when the original does.
|
||||
const fromPtr = B.tmp(),
|
||||
from = B.tmp(),
|
||||
bit = B.tmp(),
|
||||
toPtr = B.tmp(),
|
||||
to = B.tmp(),
|
||||
kind = B.tmp();
|
||||
B.line(
|
||||
`${fromPtr} = getelementptr inbounds %ScrClosure, ptr ${original}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${from} = load i32, ptr ${fromPtr}`);
|
||||
B.line(`${bit} = and i32 ${from}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${toPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
B.line(`${to} = load i32, ptr ${toPtr}`);
|
||||
B.line(`${kind} = or i32 ${to}, ${bit}`);
|
||||
B.line(`store i32 ${kind}, ptr ${toPtr}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
@@ -813,7 +837,18 @@ export function emitCallExpr(
|
||||
const ft = e.callee.type;
|
||||
if (e.args.length !== ft.params.length)
|
||||
throw new LlvmUnsupportedError("callValue:arity", e.loc);
|
||||
const callee = host.stackCallbacks.emit(host, e.callee) ?? emitBorrowedInput(host, e.callee);
|
||||
// A receiver that reads the ambient `this` binds what the callee
|
||||
// would see without a binding: the call forwards it unchanged and
|
||||
// needs no receiver bracket (signature adapters forward this way).
|
||||
const forwardsReceiver =
|
||||
e.receiver?.kind === "libCall" &&
|
||||
e.receiver.fn === "dyn.this" &&
|
||||
e.receiver.args.length === 0;
|
||||
const callee =
|
||||
host.stackCallbacks.emit(host, e.callee) ??
|
||||
(lendsCallee(host, e, forwardsReceiver)
|
||||
? host.emitReadReceiver(e.callee)
|
||||
: emitBorrowedInput(host, e.callee));
|
||||
// A class instance bound as the callee's `this` (`h.fn(x)`) is boxed
|
||||
// only if the callee reads it: the receiver window records the
|
||||
// instance (owned by this frame for the whole call) with its typed-ref
|
||||
@@ -825,7 +860,10 @@ export function emitCallExpr(
|
||||
!host.shapeHost.isErrorClass(e.receiver.value.type.className)
|
||||
? host.emitExpr(e.receiver.value)
|
||||
: null;
|
||||
const receiver = e.receiver === undefined || lazyReceiver ? null : host.emitExpr(e.receiver);
|
||||
const receiver =
|
||||
e.receiver === undefined || lazyReceiver || forwardsReceiver
|
||||
? null
|
||||
: host.emitExpr(e.receiver);
|
||||
const pushReceiver = (): void => {
|
||||
if (lazyReceiver) {
|
||||
const t = lazyReceiver.type;
|
||||
@@ -865,7 +903,7 @@ export function emitCallExpr(
|
||||
? borrowableInputs(host, e.args, false, host.referenceWrites?.summary(known!.name))
|
||||
: [];
|
||||
let dynamic = false;
|
||||
if (!direct && e.receiver === undefined) {
|
||||
if (!direct && (e.receiver === undefined || forwardsReceiver)) {
|
||||
const references = new Set<number>();
|
||||
ft.params.forEach((param, index) => {
|
||||
if (isRefCounted(param)) references.add(index);
|
||||
@@ -898,16 +936,51 @@ export function emitCallExpr(
|
||||
lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a,
|
||||
)
|
||||
: args;
|
||||
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
|
||||
// An entry through the closure's own pointer brackets the call only
|
||||
// when the closure may observe the receiver (SCR_FN_RECEIVER_FREE).
|
||||
const invoke = (
|
||||
target: string,
|
||||
bracket: boolean,
|
||||
values = args,
|
||||
indirect = false,
|
||||
): string => {
|
||||
const needsReceiver = bracket && !forwardsReceiver;
|
||||
let observes: string | null = null;
|
||||
if (needsReceiver) {
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
pushReceiver();
|
||||
if (indirect) {
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
free = B.tmp();
|
||||
observes = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${callee.name}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${free} = and i32 ${kind}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${observes} = icmp eq i32 ${free}, 0`);
|
||||
const push = B.newLabel("receiver.push"),
|
||||
pushed = B.newLabel("receiver.call");
|
||||
B.condBr(observes, push, pushed);
|
||||
B.startBlock(push);
|
||||
pushReceiver();
|
||||
B.br(pushed);
|
||||
B.startBlock(pushed);
|
||||
} else pushReceiver();
|
||||
}
|
||||
const result = e.type.kind === "void" ? "" : B.tmp();
|
||||
B.line(
|
||||
`${result ? `${result} = ` : ""}call ${host.llType(e.type)} ${target}(${argListOf(values)})`,
|
||||
);
|
||||
if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
if (observes !== null) {
|
||||
const pop = B.newLabel("receiver.pop"),
|
||||
popped = B.newLabel("receiver.done");
|
||||
B.condBr(observes, pop, popped);
|
||||
B.startBlock(pop);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
B.br(popped);
|
||||
B.startBlock(popped);
|
||||
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
return result;
|
||||
};
|
||||
const lending =
|
||||
@@ -953,11 +1026,11 @@ export function emitCallExpr(
|
||||
B.line(`${has} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(has, borrowing, owning);
|
||||
B.startBlock(borrowing);
|
||||
const lentResult = invoke(entry, true);
|
||||
const lentResult = invoke(entry, true, args, true);
|
||||
const borrowingEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(owning);
|
||||
const ownedResult = invoke(fn, true, ownedArgs());
|
||||
const ownedResult = invoke(fn, true, ownedArgs(), true);
|
||||
const owningEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -966,7 +1039,7 @@ export function emitCallExpr(
|
||||
B.line(
|
||||
`${t} = phi ${host.llType(e.type)} [ ${lentResult}, %${borrowingEnd} ], [ ${ownedResult}, %${owningEnd} ]`,
|
||||
);
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs());
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs(), true);
|
||||
else {
|
||||
// A global initializer may not have run, or a callback may enter
|
||||
// through a boundary the census cannot prove. Validate the actual
|
||||
@@ -984,7 +1057,7 @@ export function emitCallExpr(
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(fallback);
|
||||
const indirect = invoke(fn, true, ownedArgs());
|
||||
const indirect = invoke(fn, true, ownedArgs(), true);
|
||||
const fallbackEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -1358,6 +1431,22 @@ function ctorArguments(
|
||||
* walk consumes without owning it (LlEmitter.canBorrowReceiver proved every
|
||||
* reference argument borrowable and every argument edge-preserving). The
|
||||
* result stays reachable from the borrowed arguments until the consumer. */
|
||||
/** A closure value read from a location the call cannot write (a keyed
|
||||
* table entry, a field, a checked extraction of either) is entered without
|
||||
* a reference of its own: the location keeps the closure alive until the
|
||||
* call returns. Plain stable bindings already lend (emitBorrowedInput). */
|
||||
function lendsCallee(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"callValue">,
|
||||
forwardsReceiver: boolean,
|
||||
): boolean {
|
||||
const writes = host.referenceWrites;
|
||||
if (writes === null || host.canBorrowCallArgument(e.callee) || !host.canBorrowReceiver(e.callee))
|
||||
return false;
|
||||
const later = e.receiver === undefined || forwardsReceiver ? e.args : [e.receiver, ...e.args];
|
||||
return borrowableInputs(host, [e.callee, ...later], false, writes.ownWrites(e))[0] === true;
|
||||
}
|
||||
|
||||
export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call">): LlValue {
|
||||
const callee = host.fnByName.get(e.callee);
|
||||
if (!callee || !host.borrowedReturns.has(e.callee))
|
||||
|
||||
@@ -4,7 +4,6 @@ import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { mangleResolveThunk } from "../mangle.js";
|
||||
import {
|
||||
arrOps,
|
||||
elemAccess,
|
||||
@@ -76,7 +75,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
|
||||
const key = typeKey(inner);
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (!sym) {
|
||||
sym = mangleResolveThunk(host.resolveThunks.size);
|
||||
sym = host.contentSymbol("sc_rslv_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "resolveThunkFor", [inner]);
|
||||
const v = vAdapters(host.shapeHost, inner);
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface LlValue {
|
||||
* its absolute value is below `below` (a zero may be -0). Index consumers
|
||||
* use it in place of a round trip (integer-values.ts `indexInteger`). */
|
||||
wideInteger?: WideInteger;
|
||||
/** An i1 SSA value (or `true`) that is true exactly when this number is
|
||||
* not NaN: set by NaN-coded integer element reads and the wrapping sums
|
||||
* of them, so ToInt32 consumers need no floating compare. */
|
||||
ordered?: string;
|
||||
slot?: boolean;
|
||||
/** A sequence local held in a capture box; release the box itself. */
|
||||
boxed?: boolean;
|
||||
|
||||
@@ -499,10 +499,7 @@ export function emitOptionalChainReceiver(
|
||||
// needs no reference of its own while the body cannot remove an edge
|
||||
// (so nothing can overwrite the projection before the body finishes).
|
||||
const borrowReceiver =
|
||||
!multiple &&
|
||||
isRefCounted(narrowed) &&
|
||||
host.canBorrowReceiver(e.receiver) &&
|
||||
host.referenceEffects.preserves(e.body);
|
||||
!multiple && isRefCounted(narrowed) && borrowsChainReceiver(host, e.receiver, e.body);
|
||||
// An optional array read of a nullable-pointer union is the element
|
||||
// pointer itself, read inline and owned by the statement frame until
|
||||
// the body finishes; the bind slot borrows it like an emitExpr value.
|
||||
@@ -562,6 +559,16 @@ export function emitOptionalChainReceiver(
|
||||
};
|
||||
}
|
||||
|
||||
/** An optional chain's receiver can lend its payload to the bind slot when
|
||||
* the body leaves the receiver's locations alone: it removes no edge at
|
||||
* all, or it writes none of the locations the receiver reads. */
|
||||
function borrowsChainReceiver(host: LlvmEmitterContext, receiver: IrExpr, body: IrExpr): boolean {
|
||||
if (!host.canBorrowReceiver(receiver)) return false;
|
||||
if (host.referenceEffects.preserves(body)) return true;
|
||||
const writes = host.referenceWrites;
|
||||
return writes !== null && host.unwrittenRead(receiver, writes.expr(body));
|
||||
}
|
||||
|
||||
/** `a?.b ?? d` narrowed to the chain body's present arm. The body wraps a
|
||||
* value of the result type in a non-unit arm, so the default runs exactly
|
||||
* when the receiver is nullish and the intermediate result box is never
|
||||
@@ -588,16 +595,21 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
if (unitTags.length === 0 || present.length !== 1) return null;
|
||||
const B = host.B;
|
||||
const narrowed = receiverDef.arms[present[0]!]!;
|
||||
const r = host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
// A receiver read from a location the body cannot write lends its
|
||||
// payload to the bind slot for the whole body (`table[k]?.(…) ?? d`).
|
||||
const borrowed =
|
||||
isRefCounted(narrowed) && borrowsChainReceiver(host, chain.receiver, chain.body.value);
|
||||
const r =
|
||||
borrowed || host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
const bindTy = host.llType(narrowed);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${bindTy}`);
|
||||
B.line(
|
||||
`store ${bindTy} ${bindTy === "ptr" ? "null" : bindTy === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
|
||||
);
|
||||
host.ownSlot(bind, narrowed);
|
||||
if (!borrowed) host.ownSlot(bind, narrowed);
|
||||
const isUnit = host.tagInSet(r.name, receiverDef.id, unitTags);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
@@ -610,7 +622,10 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
B.line(`store ${bindTy} ${host.unionExtract(r.name, receiverDef.id, narrowed)}, ptr ${bind}`);
|
||||
const payload = borrowed
|
||||
? host.unionPeek(r.name, receiverDef.id)
|
||||
: host.unionExtract(r.name, receiverDef.id, narrowed);
|
||||
B.line(`store ${bindTy} ${payload}, ptr ${bind}`);
|
||||
host.chainSlots.set(chain.id, { name: bind, type: narrowed, slot: true });
|
||||
host.emitBranchInto(slot, chain.body.value);
|
||||
host.chainSlots.delete(chain.id);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitNullableIsTag, emitNullablePresent } from "./union-repr.js";
|
||||
import { isObjectArm } from "./nullable-unions.js";
|
||||
import { isPointerIdentityArm } from "./nullable-unions.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -908,13 +908,13 @@ export function emitDynamicExpr(
|
||||
if (!def)
|
||||
throw new InternalCompilerError(`llvm emitter bug: equality of unknown union ${e.unionId}`);
|
||||
const nullable = host.nullableUnions.get(def.id);
|
||||
if (nullable && isObjectArm(nullable)) {
|
||||
if (nullable && isPointerIdentityArm(nullable)) {
|
||||
// Object identity, and NULL === NULL for the unit arm: one compare.
|
||||
const eq = B.tmp();
|
||||
B.line(`${eq} = icmp ${e.negated ? "ne" : "eq"} ptr ${l.name}, ${r.name}`);
|
||||
return { name: eq, type: e.type };
|
||||
}
|
||||
if (nullable) {
|
||||
if (nullable && nullable.repr === "single" && nullable.arm.kind === "string") {
|
||||
// String arm: identical pointers are equal (the same string or the
|
||||
// same unit encoding); otherwise equal only when both hold strings
|
||||
// with the same bytes.
|
||||
|
||||
@@ -278,6 +278,18 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
returning("float_read_add", bin("|", bin("+", length, read("floats")), zero)),
|
||||
returning("product", bin("|", bin("*", length, length), zero)),
|
||||
returning("number_add", bin("+", length, read("bytes"))),
|
||||
returning(
|
||||
"read_products",
|
||||
bin(
|
||||
">>>",
|
||||
bin(
|
||||
"+",
|
||||
bin("*", read("bytes"), { kind: "numLit", value: 77, type: F64, loc }),
|
||||
bin("*", read("bytes"), { kind: "numLit", value: 150, type: F64, loc }),
|
||||
),
|
||||
zero,
|
||||
),
|
||||
),
|
||||
];
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const body = (name: string): string =>
|
||||
@@ -292,8 +304,10 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
expect(body(name), name).not.toContain("uint32.coerce");
|
||||
}
|
||||
// A NaN-coded read converts to 0 through the sum: the result selects 0
|
||||
// unless every NaN-coded operand is ordered.
|
||||
expect(body("read_add")).toMatch(/ = fcmp ord double .*, 0\.0$/m);
|
||||
// unless every NaN-coded operand is ordered. A read's own validity bit
|
||||
// tells, so the number is not compared.
|
||||
expect(body("read_add")).toMatch(/ = phi i1 \[ true, /);
|
||||
expect(body("read_add")).not.toMatch(/fcmp ord/);
|
||||
expect(body("read_add")).toMatch(/ = add i32 /);
|
||||
expect(body("read_add")).toMatch(/ = select i1 .*, i32 .*, i32 0$/m);
|
||||
expect(body("read_chain")).toMatch(/ = and i1 /);
|
||||
@@ -309,6 +323,12 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
expect(body(name), name).not.toMatch(/ = (add|mul) i32 /);
|
||||
}
|
||||
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
|
||||
// Products of NaN-coded reads and constants are exact integers or NaN, so
|
||||
// their sum adds in 32 bits too, selected by the reads' validity bits.
|
||||
expect(body("read_products")).toMatch(/ = mul i32 /);
|
||||
expect(body("read_products")).toMatch(/ = add i32 /);
|
||||
expect(body("read_products")).not.toContain("uint32.coerce");
|
||||
expect(body("read_products")).not.toMatch(/fcmp ord/);
|
||||
});
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, integerOrNaNRange, markWrappingOperands } from "./wrapping-sums.js";
|
||||
import { emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
|
||||
import { emitPowF64, emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
|
||||
|
||||
/** The signed integer predicates of the number compares. */
|
||||
const INTEGER_COMPARES: Record<string, string> = {
|
||||
@@ -305,7 +305,6 @@ export function emitOperatorExpr(
|
||||
"===": "oeq",
|
||||
"!==": "une",
|
||||
};
|
||||
const libm: Record<string, string> = { "**": "pow" };
|
||||
const bit: Record<string, "and" | "or" | "xor" | "shl" | "ashr" | "lshr"> = {
|
||||
"&": "and",
|
||||
"|": "or",
|
||||
@@ -367,8 +366,8 @@ export function emitOperatorExpr(
|
||||
`${result} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
|
||||
);
|
||||
const number = integerNumber(host, result, range, e.type);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
return uint32 === undefined ? number : { ...number, uint32 };
|
||||
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
|
||||
return wrapped === undefined ? number : { ...number, ...wrapped };
|
||||
}
|
||||
if (e.op === "%" && li.range.min >= 0 && ri.range.min > 0) {
|
||||
const range = { min: 0, max: Math.min(li.range.max, ri.range.max - 1) };
|
||||
@@ -411,12 +410,12 @@ export function emitOperatorExpr(
|
||||
return { name: emitPowerOfTwoQuotient(host, l.name, r.name), type: e.type };
|
||||
if (arith[e.op] !== undefined) {
|
||||
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
|
||||
// The exact integer beside the double, for index consumers.
|
||||
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
|
||||
if (uint32 !== undefined || wideInteger) {
|
||||
if (wrapped !== undefined || wideInteger) {
|
||||
const value: LlValue = { name: t, type: e.type };
|
||||
if (uint32 !== undefined) value.uint32 = uint32;
|
||||
if (wrapped !== undefined) Object.assign(value, wrapped);
|
||||
if (wideInteger) value.wideInteger = wideInteger;
|
||||
return value;
|
||||
}
|
||||
@@ -471,32 +470,8 @@ export function emitOperatorExpr(
|
||||
B.line(`${t} = ${e.op === ">>>" ? "uitofp" : "sitofp"} i32 ${result} to double`);
|
||||
return { name: t, type: e.type, uint32: result };
|
||||
} else {
|
||||
const fn = libm[e.op];
|
||||
if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
|
||||
host.declare(`declare double @${fn}(double, double)`);
|
||||
// libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where
|
||||
// `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline
|
||||
// guard selects NaN for a base of ±1 with a non-finite exponent and
|
||||
// keeps the direct pow call. A finite literal exponent, or a literal
|
||||
// base other than ±1, cannot meet those cases and needs no guard.
|
||||
if (fn === "pow" && !libmPowAgrees(e.left, e.right)) {
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const power = B.tmp();
|
||||
B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`);
|
||||
const base = B.tmp();
|
||||
const unit = B.tmp();
|
||||
const magnitude = B.tmp();
|
||||
const finite = B.tmp();
|
||||
const reject = B.tmp();
|
||||
B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`);
|
||||
B.line(`${unit} = fcmp oeq double ${base}, 1.0`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`);
|
||||
B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`);
|
||||
B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`);
|
||||
B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`);
|
||||
} else {
|
||||
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
|
||||
}
|
||||
if (e.op !== "**") throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
|
||||
return { name: emitPowF64(host, l.name, r.name, e.right), type: e.type };
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -649,7 +624,7 @@ export function regexLiteralTemplate(
|
||||
let re = host.regexInstances.get(key);
|
||||
if (!re) {
|
||||
re = {
|
||||
sym: `sc_re_${host.regexInstances.size}`,
|
||||
sym: host.contentSymbol("sc_re_", key),
|
||||
src: host.internLiteral(e.pattern),
|
||||
fl: host.internLiteral(e.flags),
|
||||
pattern: e.pattern,
|
||||
@@ -673,7 +648,7 @@ export function regexLiteralReplacer(
|
||||
let rr = host.regexReplacers.get(key);
|
||||
if (!rr) {
|
||||
rr = {
|
||||
sym: `sc_rr_${host.regexReplacers.size}`,
|
||||
sym: host.contentSymbol("sc_rr_", key),
|
||||
pattern: e.pattern,
|
||||
flags: e.flags,
|
||||
replacement,
|
||||
@@ -1473,19 +1448,3 @@ export function emitRecordLitFields(
|
||||
host.initField(ptr, type, inline, v);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether libm pow matches `**` for these operands: every case except a
|
||||
* base of ±1 with a NaN or infinite exponent agrees. */
|
||||
function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean {
|
||||
const power = numericLiteral(exponent);
|
||||
if (power !== null && Number.isFinite(power)) return true;
|
||||
const value = numericLiteral(base);
|
||||
return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1;
|
||||
}
|
||||
|
||||
function numericLiteral(expr: IrExpr): number | null {
|
||||
if (expr.kind === "numLit") return expr.value;
|
||||
if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit")
|
||||
return -expr.operand.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js
|
||||
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
|
||||
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
|
||||
import { preservesRegexInputs } from "./reference-effects.js";
|
||||
import { emitNumberMapSlot, emitStringMapEntry } from "./map-number-lookup.js";
|
||||
|
||||
/** Widen one tagged read. The source is borrowed unless the overflow map
|
||||
* supplied an owned value. Each selected reference payload is retained
|
||||
@@ -417,8 +418,8 @@ export function emitRecordKeyGet(
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const k = host.emitExpr(e.key);
|
||||
// The read runs no user code: both inputs lend for its duration.
|
||||
const [obj, k] = emitBorrowedInputs(host, [e.obj, e.key]) as [LlValue, LlValue];
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
@@ -440,6 +441,80 @@ export function emitRecordKeyGet(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
/** A keyed read whose every hit is the result's own pointer: the result
|
||||
* is a nullable-pointer union (nullable-unions.ts) with an undefined arm,
|
||||
* and each declared field and the overflow value hold exactly the result
|
||||
* type or its reference arm in a plain (non-inline) slot. A miss is the
|
||||
* immortal undefined encoding. Such a read can lend the stored pointer:
|
||||
* borrowedRecordKeyGet reads it without a reference. */
|
||||
export function pointerRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false;
|
||||
const resultType = e.type;
|
||||
const nullable = host.nullableUnions.single(resultType.unionId);
|
||||
if (nullable === null || undefinedArmTag(resultType, host.unionsById) < 0) return false;
|
||||
const exact = (t: IrType): boolean => typeEquals(t, resultType) || typeEquals(t, nullable.arm);
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
if (e.overflowOnly !== true) {
|
||||
for (const f of shape.fields)
|
||||
if (host.recordFieldInline(e.shapeId, f.name) || !exact(f.type)) return false;
|
||||
}
|
||||
return shape.indexValue === undefined || typeEquals(shape.indexValue, nullable.arm);
|
||||
}
|
||||
|
||||
/** A pointerRecordKeyGet read without a reference: the declared field or
|
||||
* overflow entry the key names, or the undefined encoding for a miss. The
|
||||
* record (and through it the stored value) must stay unchanged and alive
|
||||
* until the consumer is done with the pointer (canBorrowReceiver). */
|
||||
export function emitBorrowedRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: borrowed keyed read of a non-union");
|
||||
const resultType = e.type;
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
const key = host.canBorrowReceiver(e.key) ? host.emitReadReceiver(e.key) : host.emitExpr(e.key);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const join = B.newLabel("rkb.j");
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
for (const f of e.overflowOnly === true ? [] : shape.fields) {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(hit, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const { ptr, type } = host.recordFieldPtr(obj.name, e.shapeId, f.name);
|
||||
B.line(`store ptr ${host.loadRecordField(ptr, type, false)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
if (shape.indexValue) {
|
||||
const ovf = host.recordOvfPtr(obj.name, e.shapeId);
|
||||
const entry = emitStringMapEntry(host, ovf, key.name);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
B.line(`store ptr ${emitNumberMapSlot(host, entry, "ptr")}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const undefTag = undefinedArmTag(resultType, host.unionsById);
|
||||
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** A keyed read whose result is a scalar union (`r[k]` on a
|
||||
* `Record<string, number>`, at `number | undefined`) and whose every hit
|
||||
* is a number or boolean arm of it, or an inline field of the same union.
|
||||
@@ -735,27 +810,17 @@ export function keyedRecordReadInto(
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
||||
// The entry lookup filters absent keys inline; a hit retains the
|
||||
// stored value with its own type's retain (map-number-lookup.ts). A
|
||||
// nullable value's NULL is a stored unit arm: presence decides.
|
||||
const lh = B.newLabel("rkg.h");
|
||||
const ln = B.newLabel("rkg.n");
|
||||
let raw: string;
|
||||
if (iv.kind === "union" && host.nullableUnions.has(iv.unionId)) {
|
||||
// NULL is a stored unit arm of a nullable value: presence decides.
|
||||
host.declare(`declare zeroext i1 @scr_map_has_str(ptr, ptr)`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = call zeroext i1 @scr_map_has_str(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.condBr(has, lh, ln);
|
||||
B.startBlock(lh);
|
||||
raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
} else {
|
||||
raw = B.tmp();
|
||||
const isnull = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.condBr(isnull, ln, lh);
|
||||
B.startBlock(lh);
|
||||
}
|
||||
const entry = emitStringMapEntry(host, ovf, keyName);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const raw = host.retainHeap(emitNumberMapSlot(host, entry, "ptr"), iv);
|
||||
B.line(`store ${ty} ${surface(iv, raw, true)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
|
||||
@@ -13,7 +13,7 @@ export function streamDataAdapter(
|
||||
const key = `eed:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ee_dad_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_dad_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDataAdapter", [cbT]);
|
||||
const p = cbT.params[0];
|
||||
@@ -221,7 +221,7 @@ export function streamDoneFnFor(
|
||||
const key = `sd:${kind}:${typeKey(doneT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_sd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_sd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]);
|
||||
const errT = doneT.params[0];
|
||||
@@ -365,7 +365,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `fsren:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_fsren_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_fsren_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]);
|
||||
const param = cbT.params[0]!;
|
||||
@@ -454,7 +454,7 @@ export function cryptoIntThunkFor(
|
||||
const key = `cryptoint:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cryptoint_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cryptoint_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]);
|
||||
const d: string[] = [
|
||||
@@ -516,7 +516,7 @@ export function zlibBytesThunkFor(
|
||||
const key = `${family}bytes:${arrayBuffer ? "arraybuffer:" : ""}${mode}${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_${family}bytes_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol(`sc_${family}bytes_`, key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]);
|
||||
const d: string[] = [
|
||||
@@ -644,7 +644,7 @@ export function streamCbThunkFor(
|
||||
const key = `scb:${kind}:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_scb_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_scb_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]);
|
||||
const runtimeParams =
|
||||
|
||||
@@ -17,7 +17,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
|
||||
const key = `connect:${h2}:${typeKey(type)}`;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_connect_${host.resolveThunks.size}`;
|
||||
const symbol = host.contentSymbol("sc_connect_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "connectAdapter", [cb, h2]);
|
||||
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
|
||||
|
||||
@@ -19,7 +19,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
const key = "dgram:" + type.shapeId;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dgram_message_${host.resolveThunks.size}`;
|
||||
const symbol = host.contentSymbol("sc_dgram_message_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "messageAdapter", [type]);
|
||||
const shape = host.recordsById.get(type.shapeId);
|
||||
@@ -68,7 +68,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
|
||||
const key = `dns:${error.unionId}:${arity}`;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dns_lookup_${host.resolveThunks.size}`;
|
||||
const symbol = host.contentSymbol("sc_dns_lookup_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "lookupAdapter", [cb]);
|
||||
host.declare("declare ptr @scr_error_new(i32, ptr)");
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./
|
||||
import {
|
||||
ASSERT_VERDICT_LIB_FNS,
|
||||
LIB_FN_SYMS,
|
||||
PURE_MATH_LIB_FNS,
|
||||
mockTimerSym,
|
||||
USES_TIMERS_LIB_FNS,
|
||||
} from "./lib-shared.js";
|
||||
@@ -671,11 +672,13 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// which also takes those streams.
|
||||
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox"
|
||||
: // for await over a checked-dynamic value iterates a native
|
||||
// stream behind it (scr_dyn_for_await_stream.c)
|
||||
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
: e.fn === "stream.unboxDynStage" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox_stage"
|
||||
: // for await over a checked-dynamic value iterates a native
|
||||
// stream behind it (scr_dyn_for_await_stream.c)
|
||||
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
// The runtime's Date functions take and answer time values: a Date
|
||||
// operand passes its time value, read at once from the borrowed object,
|
||||
@@ -777,7 +780,8 @@ function emitSymbolLibCall(
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${retDecl} @${sym}(${argList})`);
|
||||
const attrs = PURE_MATH_LIB_FNS.has(e.fn) ? " nounwind willreturn memory(none)" : "";
|
||||
B.line(`${t} = call ${retDecl} @${sym}(${argList})${attrs}`);
|
||||
// The result joins its frame BEFORE the pending check so an unwind
|
||||
// releases the dummy (NULL for refcounted returns) harmlessly.
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
@@ -1196,7 +1200,7 @@ function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrT
|
||||
const key = `allTuple:${fnName}`;
|
||||
let adapter = host.resolveThunks.get(key);
|
||||
if (adapter !== undefined) return adapter;
|
||||
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
|
||||
adapter = host.contentSymbol("sc_all_tuple_", key);
|
||||
host.resolveThunks.set(key, adapter);
|
||||
host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]);
|
||||
const rc = vAdapters(host.shapeHost, tuple);
|
||||
|
||||
@@ -1027,6 +1027,54 @@ function emitPowerOfTwo(host: LlvmEmitterContext, n: string): string {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `base ** exponent` and `Math.pow(base, exponent)` as V8's math::pow
|
||||
* answers them: a literal exponent 2 is `base * base`, a literal 0.5 is
|
||||
* `sqrt(base + 0)` (+Infinity for an infinite base), and every other
|
||||
* exponent calls scr_math_pow, which applies V8's other special cases
|
||||
* before the platform pow. The platform pow is never called by its libm
|
||||
* name, which LLVM would rewrite (pow(2, x) to exp2(x), pow(x, -1) to 1/x)
|
||||
* into answers that can differ from V8's. */
|
||||
export function emitPowF64(
|
||||
host: LlvmEmitterContext,
|
||||
base: string,
|
||||
exponent: string,
|
||||
exponentExpr: IrExpr,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const literal =
|
||||
exponentExpr.kind === "numLit"
|
||||
? exponentExpr.value
|
||||
: exponentExpr.kind === "unary" &&
|
||||
exponentExpr.op === "-" &&
|
||||
exponentExpr.operand.kind === "numLit"
|
||||
? -exponentExpr.operand.value
|
||||
: null;
|
||||
const out = B.tmp();
|
||||
if (literal === 2) {
|
||||
B.line(`${out} = fmul double ${base}, ${base}`);
|
||||
return out;
|
||||
}
|
||||
if (literal === 0.5) {
|
||||
host.declare(`declare double @llvm.sqrt.f64(double)`);
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const plus = B.tmp(),
|
||||
root = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
infinite = B.tmp();
|
||||
B.line(`${plus} = fadd double ${base}, ${f64Lit(0)}`);
|
||||
B.line(`${root} = call double @llvm.sqrt.f64(double ${plus})`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${base})`);
|
||||
B.line(`${infinite} = fcmp oeq double ${magnitude}, 0x7FF0000000000000`);
|
||||
B.line(`${out} = select i1 ${infinite}, double 0x7FF0000000000000, double ${root}`);
|
||||
return out;
|
||||
}
|
||||
host.declare(`declare double @scr_math_pow(double, double)`);
|
||||
B.line(
|
||||
`${out} = call double @scr_math_pow(double ${base}, double ${exponent}) nounwind willreturn`,
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `Math.pow(2, n)`, the divisor emitPowerOfTwoQuotient takes. */
|
||||
export function isPowerOfTwoCall(e: IrExpr): boolean {
|
||||
return (
|
||||
@@ -1099,6 +1147,11 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const v = host.emitExpr(e.args[1]!);
|
||||
return { name: emitPowerOfTwo(host, v.name), type: e.type };
|
||||
}
|
||||
if (e.fn === "math.pow") {
|
||||
const base = host.emitExpr(e.args[0]!);
|
||||
const exponent = host.emitExpr(e.args[1]!);
|
||||
return { name: emitPowF64(host, base.name, exponent.name, e.args[1]!), type: e.type };
|
||||
}
|
||||
if (e.fn === "math.floor" || e.fn === "math.ceil") {
|
||||
const intr = e.fn === "math.floor" ? "floor" : "ceil";
|
||||
const v = host.emitExpr(e.args[0]!);
|
||||
|
||||
@@ -22,7 +22,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
const key = "sni:" + typeKey(type);
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_sni_answer_${host.resolveThunks.size}`;
|
||||
const symbol = host.contentSymbol("sc_sni_answer_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "sniAnswer", [type]);
|
||||
const params = ["ptr %self"],
|
||||
@@ -95,11 +95,12 @@ export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgum
|
||||
let type = cb.type,
|
||||
value = cb.name;
|
||||
if (type.kind === "union") {
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const unionId = type.unionId;
|
||||
const arms = host.unionsById.get(unionId)?.arms ?? [];
|
||||
const tag = arms.findIndex((arm) => arm.kind === "func");
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
value = host.unwrapNullableClosure(value, unionId, tag);
|
||||
} else host.toHeap(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined)
|
||||
throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
|
||||
@@ -37,7 +37,7 @@ function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func"
|
||||
const key = "lookup:" + typeKey(answer);
|
||||
let symbol = host.resolveThunks.get(key);
|
||||
if (symbol === undefined) {
|
||||
symbol = `sc_lookup_answer_${host.resolveThunks.size}`;
|
||||
symbol = host.contentSymbol("sc_lookup_answer_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "lookupAnswer", [answer]);
|
||||
for (const declaration of [
|
||||
|
||||
@@ -165,7 +165,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
||||
if (funcTag < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its func arm`);
|
||||
cb = host.unwrapNullableClosure(args[cbIndex]!.name, funcTag);
|
||||
cb = host.unwrapNullableClosure(args[cbIndex]!.name, cbT.unionId, funcTag);
|
||||
}
|
||||
}
|
||||
const valueCount = reusePort ? 5 : 4;
|
||||
|
||||
@@ -497,7 +497,7 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
|
||||
const key = `alsbound:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_alsbound_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_alsbound_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "boundCallbackThunk", [cbT]);
|
||||
host.declare("declare ptr @scr_als_bound_target(ptr)");
|
||||
|
||||
@@ -35,6 +35,31 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
|
||||
["assert.partialDeepResult", null],
|
||||
]);
|
||||
|
||||
/** The Math functions that call scr_ieee754.c. They read and write no
|
||||
* memory, so calls with the same argument fold and loops may hoist them. */
|
||||
export const PURE_MATH_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"math.sin",
|
||||
"math.sinh",
|
||||
"math.cos",
|
||||
"math.cosh",
|
||||
"math.tan",
|
||||
"math.tanh",
|
||||
"math.asin",
|
||||
"math.asinh",
|
||||
"math.acos",
|
||||
"math.acosh",
|
||||
"math.atan",
|
||||
"math.atanh",
|
||||
"math.cbrt",
|
||||
"math.exp",
|
||||
"math.expm1",
|
||||
"math.log",
|
||||
"math.log1p",
|
||||
"math.log2",
|
||||
"math.log10",
|
||||
"math.atan2",
|
||||
]);
|
||||
|
||||
export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
|
||||
"assert.throwsValidate": "scr_assert_throws_validate",
|
||||
@@ -118,27 +143,29 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"str.codePoints": "scr_str_code_points",
|
||||
"num.fromString": "scr_string_to_number",
|
||||
"math.round": "scr_math_round",
|
||||
"math.sin": "sin",
|
||||
"math.sinh": "sinh",
|
||||
"math.cos": "cos",
|
||||
"math.cosh": "cosh",
|
||||
"math.tan": "tan",
|
||||
"math.tanh": "tanh",
|
||||
"math.asin": "asin",
|
||||
"math.asinh": "asinh",
|
||||
"math.acos": "acos",
|
||||
"math.acosh": "acosh",
|
||||
"math.atan": "atan",
|
||||
"math.atanh": "atanh",
|
||||
"math.cbrt": "cbrt",
|
||||
"math.exp": "exp",
|
||||
"math.expm1": "expm1",
|
||||
// Math's transcendental functions are V8's (scr_ieee754.c), so their
|
||||
// results are the ones Node prints.
|
||||
"math.sin": "scr_ieee754_sin",
|
||||
"math.sinh": "scr_ieee754_sinh",
|
||||
"math.cos": "scr_ieee754_cos",
|
||||
"math.cosh": "scr_ieee754_cosh",
|
||||
"math.tan": "scr_ieee754_tan",
|
||||
"math.tanh": "scr_ieee754_tanh",
|
||||
"math.asin": "scr_ieee754_asin",
|
||||
"math.asinh": "scr_ieee754_asinh",
|
||||
"math.acos": "scr_ieee754_acos",
|
||||
"math.acosh": "scr_ieee754_acosh",
|
||||
"math.atan": "scr_ieee754_atan",
|
||||
"math.atanh": "scr_ieee754_atanh",
|
||||
"math.cbrt": "scr_ieee754_cbrt",
|
||||
"math.exp": "scr_ieee754_exp",
|
||||
"math.expm1": "scr_ieee754_expm1",
|
||||
"math.sqrt": "sqrt",
|
||||
"math.log": "log",
|
||||
"math.log1p": "log1p",
|
||||
"math.log2": "log2",
|
||||
"math.log10": "log10",
|
||||
"math.atan2": "atan2",
|
||||
"math.log": "scr_ieee754_log",
|
||||
"math.log1p": "scr_ieee754_log1p",
|
||||
"math.log2": "scr_ieee754_log2",
|
||||
"math.log10": "scr_ieee754_log10",
|
||||
"math.atan2": "scr_ieee754_atan2",
|
||||
"math.pow": "scr_math_pow",
|
||||
// encodeURIComponent throws URIError on a lone surrogate
|
||||
// (MAY_THROW_LIB_FNS); decodeUriComponent is with the throwing tier below.
|
||||
@@ -286,6 +313,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"module.id": "scr_module_id",
|
||||
"module.path": "scr_module_path",
|
||||
"module.paths": "scr_module_paths",
|
||||
"module.requireComputed": "scr_require_computed",
|
||||
"module.resolveComputed": "scr_require_resolve",
|
||||
"module.children": "scr_module_children",
|
||||
"module.parent": "scr_module_parent",
|
||||
"module.loaded": "scr_module_loaded",
|
||||
@@ -818,6 +847,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"stream.stdioWritable": "scr_stream_stdio_writable",
|
||||
"stream.unboxDyn": "scr_stream_unbox_dyn",
|
||||
"stream.dynIterableSource": "scr_stream_dyn_iterable_source",
|
||||
"stream.dynSourceValue": "scr_stream_dyn_source_value",
|
||||
"stream.unboxDynStage": "scr_stream_unbox_dyn_stage",
|
||||
"stream.dynTransformResult": "scr_stream_dyn_transform_result",
|
||||
"stream.handleStream": "scr_stream_handle",
|
||||
"stream.pipeDyn": "scr_stream_pipe_dyn",
|
||||
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
|
||||
@@ -1683,6 +1715,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objEntries": "scr_dyn_obj_entries",
|
||||
"dyn.structuredClone": "scr_structured_clone",
|
||||
"dyn.declAdopt": "scr_dyn_decl_adopt",
|
||||
"dyn.adoptShared": "scr_dyn_adopt_shared",
|
||||
"dyn.cloneMissing": "scr_structured_clone_missing",
|
||||
"dyn.cloneTransferFail": "scr_structured_clone_transfer_fail",
|
||||
"dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with",
|
||||
|
||||
@@ -743,7 +743,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const helpers = packed(5 + n + k, helperCount);
|
||||
let at = 5 + n + k + helperCount;
|
||||
const take = (present: boolean): string => (present ? args[at++]!.name : "null");
|
||||
const box = take(((flags >> 1) & 3) === 2);
|
||||
const box = take(((flags >> 1) & 3) >= 2);
|
||||
const signal = take((flags & 32) !== 0);
|
||||
const abort = take((flags & 32) !== 0);
|
||||
const head =
|
||||
|
||||
@@ -12,13 +12,18 @@ import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import {
|
||||
emitCompactMapSlot,
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitNumberMapSlot,
|
||||
emitStringMapEntry,
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapGetRef,
|
||||
compactMapValues,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
import { rememberMapEntry } from "./map-entry-fusion.js";
|
||||
|
||||
@@ -490,3 +495,62 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.line(`store i32 ${selected}, ptr ${tag}`);
|
||||
return { value: { name: box, type: read.type }, owner };
|
||||
}
|
||||
|
||||
/** A string-keyed Map.get whose result is a nullable-pointer union over the
|
||||
* map's own value type (`Map<string, F>` read as `F | undefined`): a hit is
|
||||
* the stored pointer itself and a miss the immortal undefined encoding, so
|
||||
* the read can lend the stored value (emitBorrowedMapGet). */
|
||||
export function pointerMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (
|
||||
e.kind !== "mapIntrinsic" ||
|
||||
e.method !== "get" ||
|
||||
e.args.length !== 1 ||
|
||||
e.type.kind !== "union" ||
|
||||
e.receiver.type.kind !== "map" ||
|
||||
e.receiver.type.key.kind !== "string" ||
|
||||
!borrowsMapReadInputs(e)
|
||||
)
|
||||
return false;
|
||||
const nullable = host.nullableUnions.single(e.type.unionId);
|
||||
return (
|
||||
nullable !== null &&
|
||||
typeEquals(nullable.arm, e.receiver.type.value) &&
|
||||
undefinedArmTag(e.type, host.unionsById) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
/** A pointerMapGet read without a reference: the map (and through it the
|
||||
* stored value) must stay unchanged and alive until the consumer is done
|
||||
* with the pointer (canBorrowReceiver). */
|
||||
export function emitBorrowedMapGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "mapIntrinsic" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const mapType = e.receiver.type;
|
||||
if (e.type.kind !== "union" || mapType.kind !== "map")
|
||||
throw new InternalCompilerError("llvm emitter bug: borrowed Map.get shape");
|
||||
const receiver = host.emitReadReceiver(e.receiver);
|
||||
const keyExpr = e.args[0]!;
|
||||
const key = host.canBorrowReceiver(keyExpr)
|
||||
? host.emitReadReceiver(keyExpr)
|
||||
: host.emitExpr(keyExpr);
|
||||
const compact = compactMapValues(host, mapType.key, mapType.value);
|
||||
const entry = emitStringMapEntry(host, receiver.name, key.name, compact);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const hit = B.newLabel("mapb.hit"),
|
||||
miss = B.newLabel("mapb.miss"),
|
||||
join = B.newLabel("mapb.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const value = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr");
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const absent = host.unitInstanceRef(e.type.unionId, undefinedArmTag(e.type, host.unionsById));
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${value}, %${hit} ], [ ${absent}, %${miss} ]`);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type IrType,
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { NullableUnions } from "./nullable-unions.js";
|
||||
import { isObjectArm, isPointerIdentityArm, NullableUnions } from "./nullable-unions.js";
|
||||
|
||||
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
|
||||
const a: IrType = { kind: "record", shapeId: "a" };
|
||||
@@ -111,3 +111,22 @@ test("library, lifetime-region and debug builds keep record unions boxed", () =>
|
||||
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("function arms are nullable pointers compared by identity root", () => {
|
||||
const fn: IrType = { kind: "func", params: [F64], ret: F64 };
|
||||
const unions = new NullableUnions(
|
||||
module([
|
||||
{ id: "fu", arms: [fn, UNDEFINED_T] },
|
||||
{ id: "fnu", arms: [NULL_T, fn, UNDEFINED_T] },
|
||||
{ id: "ff", arms: [fn, F64] },
|
||||
]),
|
||||
);
|
||||
expect(unions.single("fu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: fn });
|
||||
expect(unions.single("fnu")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0 });
|
||||
expect(unions.get("ff")).toBeNull();
|
||||
const single = unions.single("fu")!;
|
||||
// Always truthy, but `===` compares adapters by the function they adapt.
|
||||
expect(isObjectArm(single)).toBe(true);
|
||||
expect(isPointerIdentityArm(single)).toBe(false);
|
||||
expect(unions.armTypes().get("fu")).toEqual(fn);
|
||||
});
|
||||
|
||||
@@ -89,21 +89,30 @@ export const NULLABLE_ABSENT = "@sc_nullable_absent";
|
||||
|
||||
/** The reference arms whose instances are never NULL. Class and record
|
||||
* entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
|
||||
* sets retain through a NULL-skipping wrapper (shapes.ts). Object arms are
|
||||
* always truthy and compare by identity; string arms are truthy when
|
||||
* non-empty and compare bytes. */
|
||||
* sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
|
||||
* (function arms). Object arms are always truthy and compare by identity
|
||||
* (function arms by identity root); string arms are truthy when non-empty
|
||||
* and compare bytes. */
|
||||
export type NullableRefArm =
|
||||
| { kind: "object"; className: string }
|
||||
| { kind: "record"; shapeId: string }
|
||||
| { kind: "string" }
|
||||
| { kind: "stats" }
|
||||
| (IrType & { kind: "array" | "map" | "set" });
|
||||
| (IrType & { kind: "array" | "map" | "set" | "func" });
|
||||
|
||||
/** The arm is a JS object: always truthy, compared by identity. */
|
||||
/** The arm is a JS object: always truthy. */
|
||||
export function isObjectArm(nullable: PointerUnion): boolean {
|
||||
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
|
||||
}
|
||||
|
||||
/** The arm compares by pointer identity. Function arms compare identity
|
||||
* roots instead: a signature adapter is the function it adapts. */
|
||||
export function isPointerIdentityArm(nullable: PointerUnion): boolean {
|
||||
return (
|
||||
nullable.repr === "shapes" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func")
|
||||
);
|
||||
}
|
||||
|
||||
/** The representation decision for every union of one module. It is a
|
||||
* pure function of the module, so serialized-IR and in-memory emission
|
||||
* agree. */
|
||||
@@ -264,7 +273,8 @@ function nullableShape(
|
||||
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
|
||||
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
|
||||
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
|
||||
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set") return { ...base, arm };
|
||||
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
|
||||
return { ...base, arm };
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,24 @@ import type { LlValue } from "./expr-context.js";
|
||||
import { guardedValue } from "./borrowed-receivers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
|
||||
import {
|
||||
emitBorrowedMapGet,
|
||||
emitStackMapRead,
|
||||
matchMapRead,
|
||||
pointerMapGet,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
|
||||
import { emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { borrowsCheckedNarrow, emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { borrowableInputs } from "./borrowed-inputs.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import {
|
||||
emitBorrowedRecordKeyGet,
|
||||
emitStackRecordKeyGet,
|
||||
pointerRecordKeyGet,
|
||||
scalarRecordKeyGet,
|
||||
} from "./expr-records.js";
|
||||
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
@@ -92,6 +102,10 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
return emitBorrowedResultCall(emitter, e);
|
||||
const arrayRead = emitter.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead);
|
||||
if (e.kind === "recordKeyGet" && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedRecordKeyGet(emitter, e);
|
||||
if (e.kind === "mapIntrinsic" && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedMapGet(emitter, e);
|
||||
const narrow = emitter.checkedNarrows.get(e);
|
||||
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
|
||||
if (narrowed) return narrowed;
|
||||
@@ -294,7 +308,27 @@ export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
// snapshot, so neither the callee nor a later argument may replace
|
||||
// what it was read from: a callee that preserves every edge cannot,
|
||||
// and any other one must leave the argument's locations unwritten.
|
||||
// A keyed read lends the stored pointer while the record keeps it.
|
||||
case "recordKeyGet":
|
||||
return (
|
||||
pointerRecordKeyGet(emitter, e) &&
|
||||
emitter.canBorrowReceiver(e.obj) &&
|
||||
emitter.referenceEffects.preserves(e.key)
|
||||
);
|
||||
// So does a Map.get while the map keeps it.
|
||||
case "mapIntrinsic":
|
||||
return (
|
||||
pointerMapGet(emitter, e) &&
|
||||
emitter.canBorrowReceiver(e.receiver) &&
|
||||
emitter.referenceEffects.preserves(e.args[0]!)
|
||||
);
|
||||
case "call": {
|
||||
// A checked extraction lends the payload of the union it narrows
|
||||
// (emitCheckedNarrowReceiver).
|
||||
if (borrowsCheckedNarrow(emitter, e)) {
|
||||
const narrow = emitter.checkedNarrows.get(e)!;
|
||||
return emitter.canBorrowReceiver(narrow.value);
|
||||
}
|
||||
if (!emitter.borrowedReturns.has(e.callee)) return false;
|
||||
if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false;
|
||||
const preserving = emitter.referenceEffects.functions.has(e.callee);
|
||||
|
||||
@@ -97,3 +97,46 @@ test("an array that is read back or escapes keeps its element writes", () => {
|
||||
const input = fill("input", [store("input", element("input"))]);
|
||||
expect(writes(input).conflicts(["a"], writes(input).summary("input"))).toBe(true);
|
||||
});
|
||||
|
||||
test("a keyed record read depends on every field and keyed entry of records", () => {
|
||||
const handler: IrType = { kind: "func", params: [F64], ret: F64 };
|
||||
const table: IrType = { kind: "record", shapeId: "t" };
|
||||
const key: IrExpr = { kind: "strLit", value: "k", type: { kind: "string" }, loc };
|
||||
const read: IrExpr = {
|
||||
kind: "recordKeyGet",
|
||||
obj: ref("table", table),
|
||||
shapeId: "t",
|
||||
key,
|
||||
type: { kind: "union", unionId: "hu" },
|
||||
loc,
|
||||
};
|
||||
const keyedWrite: IrStmt = {
|
||||
kind: "recordKeySet",
|
||||
obj: ref("table", table),
|
||||
shapeId: "t",
|
||||
key,
|
||||
value: ref("h", handler),
|
||||
loc,
|
||||
};
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
name,
|
||||
loc,
|
||||
returnType: F64,
|
||||
params: [{ localId: "table", name: "table", type: table }],
|
||||
locals: [
|
||||
{ id: "table", name: "table", type: table, mutable: false },
|
||||
{ id: "h", name: "h", type: handler, mutable: false },
|
||||
],
|
||||
body,
|
||||
});
|
||||
const records = [{ id: "t", fields: [{ name: "add", type: handler }], indexValue: handler }];
|
||||
const quiet = fn("quiet", []);
|
||||
const writer = fn("writer", [keyedWrite]);
|
||||
const functions = new Map([quiet, writer].map((f) => [f.name, f]));
|
||||
const rw = new ReferenceWrites(functions, [], () => false, new Set(), { records });
|
||||
rw.enter(quiet);
|
||||
const keys = rw.readKeys(read);
|
||||
expect(keys).toEqual(expect.arrayContaining(["r:*", "r:add"]));
|
||||
expect(rw.conflicts(keys!, rw.summary("quiet"))).toBe(false);
|
||||
expect(rw.conflicts(keys!, rw.summary("writer"))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -92,6 +92,8 @@ const INERT_LIB_PREFIXES = [
|
||||
];
|
||||
const INERT_LIB_CALLS = new Set([
|
||||
"dyn.compare",
|
||||
// Reads the ambient receiver (a fresh reference to the bound value).
|
||||
"dyn.this",
|
||||
"error.new",
|
||||
"error.nodeThrow",
|
||||
"dyn.typedRefIs",
|
||||
@@ -923,6 +925,17 @@ export class ReferenceWrites {
|
||||
case "arrayGet":
|
||||
out.push(ARRAY_KEY);
|
||||
return this.expr(node.index) === null && walk(node.arr);
|
||||
case "recordKeyGet":
|
||||
// Any declared field or overflow entry the key may name.
|
||||
out.push(ANY_RECORD_KEY);
|
||||
for (const f of this.records.get(node.shapeId)?.fields ?? []) out.push(`r:${f.name}`);
|
||||
return this.expr(node.key) === null && walk(node.obj);
|
||||
case "mapIntrinsic":
|
||||
// A typed Map.get: any entry of any map.
|
||||
if (node.method !== "get" || node.args.length !== 1 || !borrowsMapReadInputs(node))
|
||||
return false;
|
||||
out.push(MAP_KEY);
|
||||
return this.expr(node.args[0]!) === null && walk(node.receiver);
|
||||
case "unionNarrow":
|
||||
case "upcast":
|
||||
case "downcast":
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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";
|
||||
|
||||
async function llvmFor(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-shape-union-tags-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return emitLlvmModule(module);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function tagHelpers(ir: string): string[] {
|
||||
return [...ir.matchAll(/^define internal i32 @sc_stag\.[^\n]*\{[^]*?^}/gm)].map(
|
||||
(match) => match[0],
|
||||
);
|
||||
}
|
||||
|
||||
test("a union whose arms span few shape numbers switches on the shape word", async () => {
|
||||
const ir = await llvmFor(`
|
||||
type Num = { kind: "num"; value: number };
|
||||
type Neg = { kind: "neg"; operand: Node };
|
||||
type Add = { kind: "add"; left: Node; right: Node };
|
||||
type Node = Num | Neg | Add;
|
||||
function evaluate(node: Node): number {
|
||||
switch (node.kind) {
|
||||
case "num": return node.value;
|
||||
case "neg": return -evaluate(node.operand);
|
||||
case "add": return evaluate(node.left) + evaluate(node.right);
|
||||
}
|
||||
}
|
||||
const one: Node = { kind: "num", value: 1 };
|
||||
console.log(evaluate({ kind: "add", left: one, right: { kind: "neg", operand: one } }));
|
||||
`);
|
||||
expect(ir).not.toMatch(/@sc_stagtab\./);
|
||||
const helpers = tagHelpers(ir);
|
||||
expect(helpers.length).toBeGreaterThan(0);
|
||||
for (const helper of helpers) expect(helper).toMatch(/switch i32 %w/);
|
||||
});
|
||||
|
||||
test("a union whose arms span many shape numbers reads the arm from its table", async () => {
|
||||
const arms = Array.from({ length: 40 }, (_, i) => i);
|
||||
const ir = await llvmFor(`
|
||||
${arms.map((i) => `type K${i} = { kind: "k${i}"; v${i}: number };`).join("\n")}
|
||||
type Any = ${arms.map((i) => `K${i}`).join(" | ")};
|
||||
function value(item: Any): number {
|
||||
switch (item.kind) {
|
||||
${arms.map((i) => ` case "k${i}": return item.v${i};`).join("\n")}
|
||||
}
|
||||
}
|
||||
const items: Any[] = [${arms.map((i) => `{ kind: "k${i}", v${i}: ${i} }`).join(", ")}];
|
||||
let total = 0;
|
||||
for (const item of items) total += value(item);
|
||||
console.log(total);
|
||||
`);
|
||||
// One byte per shape number from the arms' lowest to highest; 255 names no arm.
|
||||
const table =
|
||||
/^(@sc_stagtab\.\S+) = private unnamed_addr constant \[(\d+) x i8\] \[([^\]]*)\]$/m.exec(ir);
|
||||
expect(table, "arm table").not.toBeNull();
|
||||
const entries = table![3]!.split(", ").map((entry) => Number(entry.replace("i8 ", "")));
|
||||
expect(entries).toHaveLength(Number(table![2]));
|
||||
expect(entries.filter((entry) => entry !== 255).sort((a, b) => a - b)).toEqual(arms);
|
||||
const helper = tagHelpers(ir).find((text) => text.includes(table![1]!));
|
||||
expect(helper, "tag helper").toBeDefined();
|
||||
expect(helper).toContain(`getelementptr inbounds [${table![2]} x i8], ptr ${table![1]}`);
|
||||
expect(helper).not.toMatch(/switch i32 %w/);
|
||||
});
|
||||
@@ -313,19 +313,46 @@ function shapeUnionRc(host: ShapeHost, union: ShapeUnion): { retain: string; rel
|
||||
};
|
||||
}
|
||||
|
||||
/** The widest span of shape numbers a union's arms may cover for its tag
|
||||
* helper to switch on the shape word (emitShapeUnionHelpers). */
|
||||
const SHAPE_TAG_SWITCH_SPAN = 32;
|
||||
|
||||
/** The tag, release and trace helpers of every shape union
|
||||
* (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
|
||||
* the null sentinel are the unit arms, anything else a tagged record whose
|
||||
* shape word names its arm (the switch folds to a lookup table). */
|
||||
* shape word names its arm.
|
||||
* - Arms within SHAPE_TAG_SWITCH_SPAN shape numbers: the helper switches on
|
||||
* the shape word. Inlined, the switch folds into the caller's branch on
|
||||
* the arm, one indirect jump with a jump table of at most that many
|
||||
* entries per site.
|
||||
* - Wider unions: the shape word indexes the union's arm table, one entry
|
||||
* per shape number from the arms' lowest to highest, and callers branch
|
||||
* on the small arm tag. Folded switches would give every site of such a
|
||||
* union a jump table spanning all of its arms' shape numbers. */
|
||||
export function emitShapeUnionHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
const S = host.sizeType;
|
||||
for (const union of host.nullableUnions.allShapeUnions()) {
|
||||
const header = shapeWordHeader(S);
|
||||
const tagFn = shapeTagFn(union.unionId);
|
||||
const cases = union.arms
|
||||
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
|
||||
.join(" ");
|
||||
const numbers = union.arms.map((arm) => host.nullableUnions.shapeNumber(arm.shapeId));
|
||||
const lo = Math.min(...numbers);
|
||||
const span = Math.max(...numbers) - lo + 1;
|
||||
const byTable = span > SHAPE_TAG_SWITCH_SPAN;
|
||||
// Entries are bytes, or 16-bit words for a union of 255 arms or more.
|
||||
// A shape number that names no arm maps to the all-ones entry, which is
|
||||
// above every arm tag, so callers take their path for no arm as they do
|
||||
// for the -1 of a shape number outside the table.
|
||||
const wide = union.arms.some((arm) => arm.tag >= 255);
|
||||
const entry = wide ? "i16" : "i8";
|
||||
const none = wide ? 65535 : 255;
|
||||
const table = new Array<number>(span).fill(none);
|
||||
union.arms.forEach((arm, i) => (table[numbers[i]! - lo] = arm.tag));
|
||||
const tableName = tagFn.replace("@sc_stag.", "@sc_stagtab.");
|
||||
if (byTable)
|
||||
out.push(
|
||||
`${tableName} = private unnamed_addr constant [${span} x ${entry}] [${table.map((t) => `${entry} ${t}`).join(", ")}]`,
|
||||
);
|
||||
out.push(
|
||||
`define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
|
||||
`entry:`,
|
||||
@@ -343,16 +370,35 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] {
|
||||
` ret i32 ${union.nullTag}`,
|
||||
`rec:`,
|
||||
);
|
||||
out.push(
|
||||
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` switch i32 %w, label %bad [ ${cases} ]`,
|
||||
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
|
||||
`bad:`,
|
||||
` ret i32 -1`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
const cases = union.arms.map((arm, i) => `i32 ${numbers[i]}, label %a${arm.tag}`).join(" ");
|
||||
if (!byTable)
|
||||
out.push(
|
||||
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` switch i32 %w, label %bad [ ${cases} ]`,
|
||||
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
|
||||
`bad:`,
|
||||
` ret i32 -1`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
else
|
||||
out.push(
|
||||
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` %i = sub i32 %w, ${lo}`,
|
||||
` %inrange = icmp ult i32 %i, ${span}`,
|
||||
` br i1 %inrange, label %look, label %bad`,
|
||||
`look:`,
|
||||
` %tp = getelementptr inbounds [${span} x ${entry}], ptr ${tableName}, i32 0, i32 %i`,
|
||||
` %t8 = load ${entry}, ptr %tp`,
|
||||
` %tag = zext ${entry} %t8 to i32`,
|
||||
` ret i32 %tag`,
|
||||
`bad:`,
|
||||
` ret i32 -1`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
// Release and trace: one definition for the unions over the same
|
||||
// record arms (helperOwner). Unit encodings are NULL or the immortal
|
||||
// null sentinel; the shape word picks the arm.
|
||||
|
||||
@@ -32,10 +32,12 @@ export class StackCallbacks {
|
||||
const count = B.tmp();
|
||||
B.line(`${count} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${value.captures.length}, ptr ${count}`);
|
||||
if (this.functions.get(value.fnName)?.ownsPrototype) {
|
||||
const target = this.functions.get(value.fnName);
|
||||
const functionKind = target ? host.closureFunctionKind(target) : 0;
|
||||
if (functionKind !== 0) {
|
||||
const kind = B.tmp();
|
||||
B.line(`${kind} = getelementptr inbounds %ScrClosure, ptr ${closure}, i32 0, i32 4`);
|
||||
B.line(`store i32 4, ptr ${kind}`);
|
||||
B.line(`store i32 ${functionKind}, ptr ${kind}`);
|
||||
}
|
||||
value.captures.forEach((localId, index) => {
|
||||
const box = host.retainBox(host.captureBox(localId));
|
||||
|
||||
@@ -22,8 +22,9 @@ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrEx
|
||||
/** The values an f64 expression can have when each one is an exact integer
|
||||
* (never -0) or NaN: proven ranges, safe integer literals, NaN-coded reads
|
||||
* of integer typed-array elements and of Int32[] elements, `charCodeAt`,
|
||||
* and sums and differences of those whose every integer result is still
|
||||
* exact in a double. */
|
||||
* and sums, differences and products of those whose every integer result
|
||||
* is a safe integer, which the double holds exactly. A product that can be
|
||||
* -0 (a zero times a negative) has no range. */
|
||||
export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange | null {
|
||||
if (e.type.kind !== "f64") return null;
|
||||
const range = host.integerRanges.get(e);
|
||||
@@ -51,7 +52,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
|
||||
// A UTF-16 code unit, or NaN past either end.
|
||||
if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 };
|
||||
if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null;
|
||||
if (e.kind === "bin" && (e.op === "+" || e.op === "-")) {
|
||||
if (e.kind === "bin" && (e.op === "+" || e.op === "-" || e.op === "*")) {
|
||||
const left = integerOrNaNRange(host, e.left);
|
||||
const right = left && integerOrNaNRange(host, e.right);
|
||||
return right ? integerArithmeticRange(e.op, left!, right) : null;
|
||||
@@ -113,9 +114,14 @@ function wrappingOperand(
|
||||
return { low, valid: null };
|
||||
}
|
||||
// The number is an exact integer or NaN, and its ToUint32 view is the
|
||||
// integer's low 32 bits or 0. An ordered compare tells the two apart.
|
||||
const valid = B.tmp();
|
||||
B.line(`${valid} = fcmp ord double ${value.name}, 0.0`);
|
||||
// integer's low 32 bits or 0. Its ordered bit, or an ordered compare,
|
||||
// tells the two apart.
|
||||
let valid: string | null = value.ordered ?? null;
|
||||
if (valid === "true") valid = null;
|
||||
else if (valid === null) {
|
||||
valid = B.tmp();
|
||||
B.line(`${valid} = fcmp ord double ${value.name}, 0.0`);
|
||||
}
|
||||
if (value.uint32 !== undefined) return { low: value.uint32, valid };
|
||||
// Without a view, convert once. The conversion of NaN is poison, but the
|
||||
// result only selects the sum when every operand is ordered.
|
||||
@@ -137,8 +143,9 @@ function wrappingOperand(
|
||||
* 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. */
|
||||
* to 0 like the integer 0. Returns the converted result with its ordered
|
||||
* bit (`true` when no operand can be NaN), or undefined, emitting nothing,
|
||||
* for any other expression. */
|
||||
export function emitWrappingSum(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "bin" },
|
||||
@@ -146,7 +153,7 @@ export function emitWrappingSum(
|
||||
r: LlValue,
|
||||
li: LlInteger | null,
|
||||
ri: LlInteger | null,
|
||||
): string | undefined {
|
||||
): { uint32: string; ordered: string } | 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));
|
||||
@@ -165,14 +172,14 @@ export function emitWrappingSum(
|
||||
`${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 (valid === null) return { uint32: sum, ordered: "true" };
|
||||
if (left.valid && right.valid) {
|
||||
valid = B.tmp();
|
||||
B.line(`${valid} = and i1 ${left.valid}, ${right.valid}`);
|
||||
}
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`);
|
||||
return out;
|
||||
return { uint32: out, ordered: valid };
|
||||
}
|
||||
|
||||
/** Whether every product of a value in `left` and one in `right` is an
|
||||
|
||||
@@ -221,15 +221,6 @@ export function mangleGenDrop(fnName: string): string {
|
||||
export function mangleGenSourceClose(fnName: string): string {
|
||||
return `sc_gc_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
}
|
||||
/** Emitted generator-resume result builder (the IteratorResult record),
|
||||
* interned per generator-type key. */
|
||||
export function mangleGenResThunk(n: number): string {
|
||||
return `sc_gres_${n}`;
|
||||
}
|
||||
/** Emitted Promise.race fulfillment adapter (entry inner type → result
|
||||
* inner type), interned per type pair. */
|
||||
export function mangleRaceThunk(n: number): string {
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface NativeLinkFeatures {
|
||||
* Absent (false) for programs without them. */
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
typeOrigins?: boolean;
|
||||
/** for await over checked-dynamic values: scr_dyn_for_await.c. Absent
|
||||
* (false) for programs without it. */
|
||||
dynForAwait?: boolean;
|
||||
@@ -71,6 +72,9 @@ export interface NativeLinkFeatures {
|
||||
/** Builtin functions read as values (Buffer.byteLength, Buffer.alloc,
|
||||
* fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */
|
||||
builtinValues?: boolean;
|
||||
/** createRequire's computed require and require.resolve link
|
||||
* scr_require.c; absent (false) otherwise. */
|
||||
computedRequire?: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
|
||||
@@ -126,6 +126,7 @@ export interface CcOptions {
|
||||
/** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
typeOrigins?: boolean;
|
||||
/** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */
|
||||
dynForAwait?: boolean;
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
@@ -138,6 +139,9 @@ export interface CcOptions {
|
||||
/** Buffer.prototype methods called on checked-dynamic values:
|
||||
* compiles scr_bytes_dyn.c. */
|
||||
dynBuffer?: boolean;
|
||||
/** createRequire's computed require and require.resolve: compiles
|
||||
* scr_require.c. */
|
||||
computedRequire?: boolean;
|
||||
/** Builtin functions read as values: compiles scr_builtin_values.c. */
|
||||
builtinValues?: boolean;
|
||||
/** The program constructs EventTarget/Event/CustomEvent values or
|
||||
|
||||
@@ -191,6 +191,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
|
||||
// Builtin functions read as values in JavaScript dependencies.
|
||||
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
|
||||
// createRequire's computed require and require.resolve.
|
||||
unit((u) => u.opts.computedRequire === true, "scr_require.c"),
|
||||
unit((u) => u.opts.dc, "scr_dc.c"),
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"),
|
||||
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
|
||||
@@ -302,6 +304,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
|
||||
// The declaration-typed boundary's adoption.
|
||||
unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"),
|
||||
unit((u) => u.opts.typeOrigins === true, "scr_type_origin.c"),
|
||||
// for await over checked-dynamic values.
|
||||
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
|
||||
// for await over checked-dynamic values that may hold native streams.
|
||||
|
||||
@@ -74,6 +74,8 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_slots.c",
|
||||
"scr_alloc.c",
|
||||
"scr_cipher_info.c",
|
||||
"scr_ieee754.c",
|
||||
"scr_glibc_math.c",
|
||||
] as const;
|
||||
|
||||
export function runtimeSrcDir(): string {
|
||||
|
||||
@@ -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 = 45 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v45" as const;
|
||||
export const RUNTIME_ABI_VERSION = 46 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v46" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -87,12 +87,14 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
streamDyn: boolean;
|
||||
handleProps: boolean;
|
||||
declAdopt: boolean;
|
||||
typeOrigins: boolean;
|
||||
dynForAwait: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
mime: boolean;
|
||||
utilTiming: boolean;
|
||||
dynBuffer: boolean;
|
||||
builtinValues: boolean;
|
||||
computedRequire: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
brotli: boolean;
|
||||
@@ -289,12 +291,14 @@ export function effectiveRuntimeFeatures(
|
||||
streamDyn: features.streamDyn === true,
|
||||
handleProps: features.handleProps === true,
|
||||
declAdopt: features.declAdopt === true,
|
||||
typeOrigins: features.typeOrigins === true,
|
||||
dynForAwait: features.dynForAwait === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
mime: features.mime === true,
|
||||
utilTiming: features.utilTiming === true,
|
||||
dynBuffer: features.dynBuffer === true,
|
||||
builtinValues: features.builtinValues === true,
|
||||
computedRequire: features.computedRequire === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
brotli: features.brotli === true,
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
|
||||
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -129,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 45, marker: "scr_runtime_abi_v45" },
|
||||
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -105,6 +105,11 @@ export function targetCpuField(cpu: TargetCpu | undefined): { targetCpu?: Target
|
||||
return cpu === undefined || cpu === "baseline" ? {} : { targetCpu: cpu };
|
||||
}
|
||||
|
||||
/** Cache-key parts for a dispatching build: the tiers it carries. */
|
||||
export function cpuDispatchKeyParts(cpuDispatch: boolean | undefined): string[] {
|
||||
return cpuDispatch === true ? ["cpu-dispatch-x86-64-v3"] : [];
|
||||
}
|
||||
|
||||
/** The error for a level on a target that is not x86-64, or null. */
|
||||
export function targetCpuTargetRefusal(
|
||||
cpu: TargetCpu | undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TypePatchCommandRequest } from "../frontend/type-patches/command.js";
|
||||
import { rmSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
@@ -116,6 +117,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values["profile-generate"] !== undefined ||
|
||||
values["profile-use"] !== undefined ||
|
||||
values.sparse ||
|
||||
@@ -176,6 +178,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values["profile-generate"] !== undefined ||
|
||||
values["profile-use"] !== undefined ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
@@ -205,6 +208,39 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
process.stdout.write(`${outputPath}\n`);
|
||||
return 0;
|
||||
}
|
||||
if (command === "types") {
|
||||
const sub = positionals[1];
|
||||
const rest = positionals.slice(2);
|
||||
let request: TypePatchCommandRequest;
|
||||
if (sub === "patch" && rest.length === 3) {
|
||||
request = {
|
||||
action: "patch",
|
||||
cwd: process.cwd(),
|
||||
pkg: rest[0]!,
|
||||
selector: rest[1]!,
|
||||
type: rest[2]!,
|
||||
range: values.range ?? null,
|
||||
dryRun: values["dry-run"],
|
||||
};
|
||||
} else if (
|
||||
sub === "list" &&
|
||||
rest.length === 0 &&
|
||||
values.range === undefined &&
|
||||
!values["dry-run"]
|
||||
) {
|
||||
request = { action: "list", cwd: process.cwd() };
|
||||
} else {
|
||||
fail(
|
||||
`scriptc types takes "patch <package> <selector> <type>" (with --range or --dry-run) or "list"\n\n${USAGE}`,
|
||||
);
|
||||
}
|
||||
const result = await host.typePatches(request);
|
||||
process.stdout.write(result.stdout);
|
||||
process.stderr.write(result.stderr);
|
||||
return result.exitCode;
|
||||
}
|
||||
if (values.range !== undefined || values["dry-run"])
|
||||
fail(`--range and --dry-run are scriptc types patch options\n\n${USAGE}`);
|
||||
if (values.sparse) fail(`--sparse is a merge-profiles option\n\n${USAGE}`);
|
||||
if (command !== "build" && command !== "run" && command !== "coverage") {
|
||||
fail(`unknown command "${command}"\n\n${USAGE}`);
|
||||
@@ -234,6 +270,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values["profile-generate"] !== undefined ||
|
||||
values["profile-use"] !== undefined ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
@@ -243,7 +280,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
hasNodeIslandFlags(values)
|
||||
) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--cpu-dispatch/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
|
||||
);
|
||||
}
|
||||
const profilePath = resolve(profileArg);
|
||||
@@ -289,6 +326,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
if (command === "coverage" && values["target-cpu"] !== undefined) {
|
||||
fail(`--target-cpu is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (command === "coverage" && values["cpu-dispatch"] !== undefined) {
|
||||
fail(`--cpu-dispatch is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (command === "coverage" && values["detached-exit"]) {
|
||||
fail(`--detached-exit is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
@@ -469,6 +509,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
|
||||
strip: values.strip,
|
||||
detachedExit: values["detached-exit"],
|
||||
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
|
||||
...(values["profile-generate"] === undefined
|
||||
? {}
|
||||
: { profileGenerate: values["profile-generate"] }),
|
||||
@@ -589,6 +630,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
|
||||
...(values["detached-exit"] ? { detachedExit: true } : {}),
|
||||
...targetCpuField(targetCpu),
|
||||
...(values["cpu-dispatch"] === undefined ? {} : { cpuDispatch: values["cpu-dispatch"] }),
|
||||
// The instrumented program resolves a relative pattern against its own
|
||||
// working directory; resolve it here, where the user wrote it.
|
||||
...(values["profile-generate"] === undefined
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
TypePatchCommandRequest,
|
||||
TypePatchCommandResult,
|
||||
} from "../frontend/type-patches/command.js";
|
||||
import type {
|
||||
AnalyzeOptions,
|
||||
AnalyzeResult,
|
||||
@@ -30,5 +34,7 @@ export interface CliHost {
|
||||
/** Merges raw or indexed profiles into one indexed profile; a sparse one
|
||||
* leaves out functions that never ran. */
|
||||
mergeProfiles: (outputPath: string, inputPaths: string[], sparse: boolean) => Promise<void>;
|
||||
/** `scriptc types patch|list` (frontend/type-patches/command.ts). */
|
||||
typePatches: (request: TypePatchCommandRequest) => Promise<TypePatchCommandResult>;
|
||||
run: (binary: string) => Promise<number>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface OutputOptionValues {
|
||||
strip?: boolean;
|
||||
windowsSubsystem?: string;
|
||||
detachedExit?: boolean;
|
||||
cpuDispatch?: boolean;
|
||||
profileGenerate?: string;
|
||||
profileUse?: string;
|
||||
ffi?: string;
|
||||
@@ -75,6 +76,9 @@ export function resolveOutputOptions(
|
||||
if (values.detachedExit && emit !== "exe") {
|
||||
return { ok: false, message: `--detached-exit is only supported with --emit=exe` };
|
||||
}
|
||||
if (values.cpuDispatch && emit !== "exe") {
|
||||
return { ok: false, message: `--cpu-dispatch is only supported with --emit=exe` };
|
||||
}
|
||||
if (values.profileGenerate !== undefined && values.profileUse !== undefined) {
|
||||
return { ok: false, message: `--profile-generate and --profile-use cannot be combined` };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ Usage:
|
||||
exporting the profile symbols; a profile
|
||||
with a sidecar section also gets the
|
||||
contract sidecar JSON beside the archive
|
||||
scriptc types patch <package> <selector> <type> [--range <r>] [--dry-run]
|
||||
fix a wrong declaration of an installed
|
||||
package: shows what the selector
|
||||
matches and adds the type patch to
|
||||
package.json ("| undefined" widens)
|
||||
scriptc types list the project's type patches and what
|
||||
they match
|
||||
scriptc cache warm [runtime|tls…] verify and load precompiled runtime families
|
||||
for the current compiler/SDK/target
|
||||
scriptc merge-profiles -o <out.profdata> [--sparse] <profile…>
|
||||
@@ -62,6 +69,12 @@ Options:
|
||||
executable then requires that level: v3 adds AVX2, BMI2
|
||||
and FMA, v4 adds AVX-512. native uses the build host's
|
||||
level. The runtime keeps its baseline code
|
||||
--cpu-dispatch x86-64: also compile the program's code for x86-64-v3
|
||||
(AVX2), and run that copy on CPUs that support it. The
|
||||
executable still runs on any x86-64 CPU. Not used with
|
||||
--target-cpu above baseline. The SCRIPTC_CPU_LEVEL
|
||||
environment variable (0 to 4, or a level name) caps the
|
||||
level at run time (opt in; --emit=exe only)
|
||||
--profile-generate <pattern>
|
||||
Linux: instrument the executable for profile-guided
|
||||
optimization. Each run writes a raw profile to the
|
||||
@@ -154,9 +167,12 @@ export const CLI_OPTIONS = {
|
||||
"windows-subsystem": { type: "string" },
|
||||
"detached-exit": { type: "boolean", default: false },
|
||||
"target-cpu": { type: "string" },
|
||||
"cpu-dispatch": { type: "boolean" },
|
||||
"profile-generate": { type: "string" },
|
||||
"profile-use": { type: "string" },
|
||||
sparse: { type: "boolean", default: false },
|
||||
range: { type: "string" },
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
"keep-llvm": { type: "boolean", default: true },
|
||||
"emit-ir": { type: "boolean", default: false },
|
||||
sanitize: { type: "boolean", default: false },
|
||||
|
||||
@@ -45,6 +45,11 @@ export interface CompileBaseOptions {
|
||||
* Unset or `baseline`, the code runs on any x86-64 CPU. x86-64 targets
|
||||
* only; the runtime pack keeps its baseline code. */
|
||||
targetCpu?: TargetCpu;
|
||||
/** Give the executable an x86-64-v3 copy of each program function, which
|
||||
* it runs instead of the baseline code on CPUs that support that level
|
||||
* (`SCRIPTC_CPU_LEVEL` caps the level at run time). Executables for x86-64
|
||||
* targets without a `targetCpu` only; off by default. */
|
||||
cpuDispatch?: boolean;
|
||||
/** Instrument the executable for profile-guided optimization. Each run
|
||||
* writes a raw profile to this path pattern (`%p` expands to the process
|
||||
* id; `LLVM_PROFILE_FILE` overrides the pattern at run time). Linux
|
||||
|
||||
@@ -937,6 +937,21 @@ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
};
|
||||
}
|
||||
|
||||
/** A createRequire require or require.resolve call whose specifier is
|
||||
* computed at run time, where the runtime resolves it like Node
|
||||
* (scr_require.c). Coverage lists the call as deferred: a require whose
|
||||
* specifier resolves to a module ends the program with this code, because
|
||||
* the module graph is fixed at build time. */
|
||||
export function computedRequireDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `${surface} with a computed specifier resolves at run time; a require of a specifier that resolves to a module, such as a native addon, ends the program`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: 'require("<static string literal>") compiles the module into the program; native addons need a replacement through native FFI (https://scriptc.dev/ffi#replacing-a-node-api-addon)',
|
||||
};
|
||||
}
|
||||
|
||||
/** Records are monomorphic structs, so a value's shape must match the
|
||||
* expected shape exactly OR narrow through the width-copy family
|
||||
* (recordWidthPlan / the overflow capture: target fields copied off the
|
||||
|
||||
@@ -239,6 +239,7 @@ test("early executable keys isolate compile modes, paths, implementation, checke
|
||||
{ ...f.options, detachedExit: true },
|
||||
{ ...f.options, targetCpu: "x86-64-v3" },
|
||||
{ ...f.options, targetCpu: "x86-64-v4" },
|
||||
{ ...f.options, cpuDispatch: true },
|
||||
{ ...f.options, profile: "profile-generate:/tmp/%p.profraw" },
|
||||
{ ...f.options, profile: "profile-use:digest" },
|
||||
{ ...f.options, checker: "tsc-ts-executable-identity" },
|
||||
|
||||
@@ -18,7 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js";
|
||||
import { type NativeArtifactDependency } from "../backend/native/contracts.js";
|
||||
import { optimizationKeyParts } from "../backend/optimization.js";
|
||||
import { type TargetCpu, targetCpuKeyParts } from "../backend/target-cpu.js";
|
||||
import { type TargetCpu, cpuDispatchKeyParts, targetCpuKeyParts } from "../backend/target-cpu.js";
|
||||
import { privateSiblingPath } from "../backend/build-cache.js";
|
||||
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
|
||||
import {
|
||||
@@ -74,12 +74,14 @@ export interface EarlyExecutableNativeFeatures {
|
||||
streamDyn?: boolean;
|
||||
handleProps?: boolean;
|
||||
declAdopt?: boolean;
|
||||
typeOrigins?: boolean;
|
||||
dynForAwait?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
mime?: boolean;
|
||||
utilTiming?: boolean;
|
||||
dynBuffer?: boolean;
|
||||
builtinValues?: boolean;
|
||||
computedRequire?: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
@@ -147,6 +149,8 @@ export interface EarlyExecutableCacheOptions {
|
||||
detachedExit?: true;
|
||||
/** Omitted for the x86-64 baseline (`--target-cpu`). */
|
||||
targetCpu?: TargetCpu;
|
||||
/** Omitted for builds without runtime CPU dispatch. */
|
||||
cpuDispatch?: true;
|
||||
/** Omitted without --profile-generate/--profile-use; otherwise the
|
||||
* pattern, or the digest of the profile's bytes. */
|
||||
profile?: string;
|
||||
@@ -291,6 +295,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
|
||||
...(options.detachedExit ? ["detached-exit"] : []),
|
||||
...targetCpuKeyParts(options.targetCpu),
|
||||
...cpuDispatchKeyParts(options.cpuDispatch),
|
||||
...(options.profile !== undefined ? [options.profile] : []),
|
||||
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
@@ -334,6 +339,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
|
||||
if (options.detachedExit) hash.update("detached-exit\0");
|
||||
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
|
||||
for (const part of cpuDispatchKeyParts(options.cpuDispatch)) hash.update(`${part}\0`);
|
||||
if (options.profile !== undefined) hash.update(`${options.profile}\0`);
|
||||
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
|
||||
@@ -27,6 +27,59 @@
|
||||
* scriptc-perf/results/design-decl-boundary/DESIGN.md. */
|
||||
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { declarationSelector, originAt } from "./lowering/decl-origin.js";
|
||||
|
||||
/** Per user file, the origin (lowering/decl-origin.ts) of each boundary
|
||||
* cast in source order: the package declaration the cast's type comes
|
||||
* from, so a failing cast names it. Set by planDeclProjection, reset per
|
||||
* program load. */
|
||||
const boundaryOrigins = new Map<string, (string | null)[]>();
|
||||
|
||||
export function resetDeclBoundaryOrigins(): void {
|
||||
boundaryOrigins.clear();
|
||||
}
|
||||
|
||||
/** The origin of the `index`-th boundary cast of a file, or null. */
|
||||
export function declBoundaryOrigin(fileName: string, index: number): string | null {
|
||||
return boundaryOrigins.get(fileName)?.[index] ?? null;
|
||||
}
|
||||
|
||||
/** The package declaration a projected expression's type comes from, in
|
||||
* the declaration world. */
|
||||
function castOrigin(checker: ts.TypeChecker, node: ts.Node, type: ts.Type): string | null {
|
||||
let inner = node as ts.Expression;
|
||||
while (
|
||||
ts.isParenthesizedExpression(inner) ||
|
||||
ts.isAwaitExpression(inner) ||
|
||||
ts.isNonNullExpression(inner)
|
||||
)
|
||||
inner = inner.expression;
|
||||
let decl: ts.Node | undefined;
|
||||
let suffix = "";
|
||||
try {
|
||||
if (ts.isCallExpression(inner) || ts.isNewExpression(inner)) {
|
||||
decl = checker.getResolvedSignature(inner)?.declaration;
|
||||
suffix = "()";
|
||||
} else {
|
||||
const name = ts.isPropertyAccessExpression(inner)
|
||||
? inner.name
|
||||
: ts.isIdentifier(inner)
|
||||
? inner
|
||||
: null;
|
||||
if (name === null) return null;
|
||||
let symbol = checker.getSymbolAtLocation(name);
|
||||
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias)
|
||||
symbol = checker.getAliasedSymbol(symbol);
|
||||
decl = symbol?.declarations[0];
|
||||
}
|
||||
if (decl === undefined) return null;
|
||||
const selector = declarationSelector(decl);
|
||||
if (selector === null) return null;
|
||||
return originAt(decl, `${selector}${suffix}`, checker.typeToString(type));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias names of projected types start with this prefix; the lowering
|
||||
* recognizes boundary casts and parameters by it. */
|
||||
@@ -573,6 +626,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
const declSf = input.declProgram.getSourceFile(file);
|
||||
if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue;
|
||||
const fileEdits: Edit[] = [];
|
||||
const fileOrigins: (string | null)[] = [];
|
||||
/** Candidate expressions (by js node) with their declared type, named
|
||||
* only for the casts that survive suppression. */
|
||||
const exprCandidates = new Map<ts.Node, ts.Type | "any">();
|
||||
@@ -946,6 +1000,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
const candidate = exprCandidates.get(e)!;
|
||||
const alias = candidate === "any" ? "any" : printer.project(candidate);
|
||||
if (alias === null) continue;
|
||||
// Only casts to a projected alias are boundary casts the lowering
|
||||
// counts (an `as any` cast is not one), so only they take a slot.
|
||||
if (candidate !== "any") {
|
||||
const twin = twins.get(e);
|
||||
fileOrigins.push(twin === undefined ? null : castOrigin(declChecker, twin, candidate));
|
||||
}
|
||||
const start = ts.tokenStartOfNode(e);
|
||||
const stmt = e.parent;
|
||||
let prefix = "(";
|
||||
@@ -980,6 +1040,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
|
||||
if (fileEdits.length === 0) continue;
|
||||
edits.set(file, fileEdits);
|
||||
if (fileOrigins.some((o) => o !== null)) boundaryOrigins.set(file, fileOrigins);
|
||||
}
|
||||
|
||||
if (edits.size === 0) return null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nodeThrowExpr, boolLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { dynUndefinedExpr, 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";
|
||||
@@ -15,12 +15,14 @@ import {
|
||||
type RuntimeResolveResult,
|
||||
} from "../../runtime-resolve.js";
|
||||
import {
|
||||
computedRequireDiag,
|
||||
invalidJsonModuleDiag,
|
||||
nativeAddonDiag,
|
||||
noLoweringDiag,
|
||||
unsupportedPackageImportDiag,
|
||||
} from "../../../diagnostics/diagnostic.js";
|
||||
import { NODE_BUILTIN_MODULES_V24 } from "../surfaces.js";
|
||||
import { NODE_BUILTIN_MODULES_V24, builtinModulesArrayLit } from "../surfaces.js";
|
||||
import { moduleFileName } from "../import-meta.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
@@ -138,6 +140,51 @@ export function lowerNodeModuleCall(
|
||||
};
|
||||
}
|
||||
|
||||
/** The run-time arguments of scr_require.c's computed require and
|
||||
* require.resolve after the specifier: the requiring file (Node's require
|
||||
* stack names it), the call's "file:line" for the SC2020 refusal, and the
|
||||
* builtin ids. The call joins the runtime-fence ledger, so coverage lists
|
||||
* it as deferred to runtime. */
|
||||
function computedRequireArgs(
|
||||
lowerer: Lowerer,
|
||||
surface: string,
|
||||
node: ts.Node,
|
||||
baseFile: ts.SourceFile,
|
||||
): IrExpr[] {
|
||||
const loc = locOf(node);
|
||||
lowerer.runtimeFences.push(computedRequireDiag(surface, loc));
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
return [
|
||||
strLit(moduleFileName(lowerer, baseFile), loc),
|
||||
strLit(`${loc.file}:${line}`, loc),
|
||||
builtinModulesArrayLit(loc),
|
||||
];
|
||||
}
|
||||
|
||||
/** A createRequire loader's require.resolve with a computed request, in
|
||||
* JavaScript source: Node's resolution at run time (scr_require.c), with
|
||||
* the options argument checked there. */
|
||||
function computedResolveCall(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
baseFile: ts.SourceFile,
|
||||
request: IrExpr,
|
||||
options: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "module.resolveComputed",
|
||||
args: [
|
||||
request,
|
||||
options,
|
||||
...computedRequireArgs(lowerer, "createRequire's require.resolve", node, baseFile),
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -160,18 +207,67 @@ function createRequireFence(
|
||||
};
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
/** A createRequire loader's `resolve` read as a value in JavaScript source
|
||||
* (passed on, or picked by `??`): a function that resolves its request at
|
||||
* run time, so `(options.resolve ?? require.resolve)(id)` behaves as in
|
||||
* Node. */
|
||||
export function createRequireResolveValue(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const baseFile = createRequireCalleeFileOf(lowerer, access.expression);
|
||||
if (baseFile === null || !isJsSourceFile(ts.getSourceFileOfNode(access))) {
|
||||
lowerer.noLowering(
|
||||
"createRequire's require.resolve as a value",
|
||||
access,
|
||||
'call it with a string literal (require.resolve("<specifier>")), which resolves at build time',
|
||||
);
|
||||
}
|
||||
const name = keyedHelperName(lowerer, "%createRequire.resolve", `${loc.file}:${loc.start}`);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const call = computedResolveCall(
|
||||
lowerer,
|
||||
access,
|
||||
baseFile,
|
||||
varRef("request", DYN, loc),
|
||||
varRef("options", DYN, loc),
|
||||
loc,
|
||||
);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [
|
||||
{ localId: "request", name: "request", type: DYN },
|
||||
{ localId: "options", name: "options", type: DYN },
|
||||
],
|
||||
locals: [
|
||||
{ id: "request", name: "request", type: DYN, mutable: false },
|
||||
{ id: "options", name: "options", type: DYN, mutable: false },
|
||||
],
|
||||
returnType: STRING,
|
||||
body: [{ kind: "return", value: call, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([DYN, DYN], STRING), loc },
|
||||
fnName: access.name.text,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A createRequire loader's `resolve.paths` read as a value in JavaScript
|
||||
* source: a function that throws the fence when called. */
|
||||
export function createRequireResolveFenceValue(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const surface = "createRequire's require.resolve as a value";
|
||||
const surface = "createRequire's require.resolve.paths as a value";
|
||||
const hint =
|
||||
'call it with a string literal (require.resolve("<specifier>")), which resolves at build time';
|
||||
'call it with a string literal (require.resolve.paths("<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)) {
|
||||
@@ -234,12 +330,38 @@ export function lowerCreateRequireCall(
|
||||
const cr = createRequireSpecOf(lowerer, call);
|
||||
if (cr === null) return null;
|
||||
if (cr.spec === null) {
|
||||
const args = call.arguments.nodes;
|
||||
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, undefined, "dynamic-require");
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
|
||||
// Spread arguments have no run-time resolution: they run, then the
|
||||
// call throws the fence.
|
||||
if (args.some((a) => ts.isSpreadElement(a)))
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
|
||||
// Node evaluates every argument, then requires the first.
|
||||
const stmts: IrStmt[] = [];
|
||||
let id = args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefinedExpr(loc);
|
||||
if (args.length > 1) {
|
||||
const slot = lowerer.declareHiddenLocal("%require.id", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: slot.id, init: id, loc });
|
||||
for (const arg of args.slice(1))
|
||||
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
|
||||
id = varRef(slot.id, DYN, loc);
|
||||
}
|
||||
const require: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "module.requireComputed",
|
||||
args: [id, ...computedRequireArgs(lowerer, "createRequire's require", call, cr.baseFile)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return lowerer.coerceInto(
|
||||
call,
|
||||
stmts.length === 0 ? require : { kind: "seqExpr", stmts, result: require, type: DYN, loc },
|
||||
lowerer.irTypeOf(call),
|
||||
);
|
||||
}
|
||||
const spec = cr.spec;
|
||||
if (spec === "node:ffi") {
|
||||
@@ -492,6 +614,26 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio
|
||||
);
|
||||
}
|
||||
const specifier = staticString(call.arguments.nodes[0]);
|
||||
// A computed request (or options) through a createRequire loader in
|
||||
// JavaScript source resolves at run time.
|
||||
if (
|
||||
(specifier === null || staticResolveOptionPaths(call.arguments.nodes[1]) === null) &&
|
||||
!pathsCall &&
|
||||
createRequireCalleeFileOf(lowerer, receiver) !== null &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call))
|
||||
) {
|
||||
const [request, options] = call.arguments.nodes;
|
||||
return computedResolveCall(
|
||||
lowerer,
|
||||
call,
|
||||
baseFile,
|
||||
lowerer.lowerExprExpecting(request!, DYN),
|
||||
options === undefined
|
||||
? dynUndefinedExpr(locOf(call))
|
||||
: lowerer.lowerExprExpecting(options, DYN),
|
||||
locOf(call),
|
||||
);
|
||||
}
|
||||
if (specifier === null) {
|
||||
const fenced = deferred("with a computed request", resolveHint);
|
||||
if (fenced) return fenced;
|
||||
|
||||
@@ -81,6 +81,7 @@ import { isClassCallback } from "./class-callbacks.js";
|
||||
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
|
||||
import { refreshDescriptorGuards } from "./class-descriptors.js";
|
||||
import { ClassConstructionDispatch } from "./class-construction.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { DispatchSites } from "./dispatch-sites.js";
|
||||
import {
|
||||
emitterRooted,
|
||||
@@ -3915,7 +3916,7 @@ export class ClassDynamicDispatch {
|
||||
if (!dispatch.fn.params.some((param) => param.localId === dispatch.keyLocal))
|
||||
params.push({ localId: dispatch.keyLocal, name: "key", type: STRING });
|
||||
const helper: IrFunction = {
|
||||
name: `%dyn.class.keys.${lowerer.liftedFns.length}`,
|
||||
name: keyedHelperName(lowerer, "%dyn.class.keys", helperKey),
|
||||
params,
|
||||
returnType: dispatch.fn.returnType,
|
||||
locals: params.map((p) => ({
|
||||
@@ -6793,24 +6794,41 @@ export class ClassDynamicDispatch {
|
||||
type: result,
|
||||
loc,
|
||||
};
|
||||
// A void method dispatches its overrides with statements: a ternary
|
||||
// has no void form.
|
||||
let voidDispatch: IrStmt[] = [{ kind: "exprStmt", expr: call, loc }];
|
||||
for (const override of overrides.toReversed()) {
|
||||
const subType: IrType = { kind: "object", className: override.cls.def.name };
|
||||
const isOverride: IrExpr = {
|
||||
kind: "instanceOf",
|
||||
value: receiver,
|
||||
className: override.cls.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const overrideCall: IrExpr = {
|
||||
kind: "call",
|
||||
callee: override.callee,
|
||||
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
|
||||
type: result,
|
||||
loc,
|
||||
};
|
||||
if (result.kind === "void") {
|
||||
voidDispatch = [
|
||||
{
|
||||
kind: "if",
|
||||
cond: isOverride,
|
||||
then: [{ kind: "exprStmt", expr: overrideCall, loc }],
|
||||
else_: voidDispatch,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
continue;
|
||||
}
|
||||
call = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "instanceOf",
|
||||
value: receiver,
|
||||
className: override.cls.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: {
|
||||
kind: "call",
|
||||
callee: override.callee,
|
||||
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
|
||||
type: result,
|
||||
loc,
|
||||
},
|
||||
cond: isOverride,
|
||||
then: overrideCall,
|
||||
else_: call,
|
||||
type: result,
|
||||
loc,
|
||||
@@ -6818,10 +6836,7 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
let body: IrStmt[];
|
||||
if (result.kind === "void")
|
||||
body = [
|
||||
{ kind: "exprStmt", expr: call, loc },
|
||||
{ kind: "return", value: dynUndefinedExpr(loc), loc },
|
||||
];
|
||||
body = [...voidDispatch, { kind: "return", value: dynUndefinedExpr(loc), loc }];
|
||||
else {
|
||||
const boxed = lowerer.coerceToExpected(call, DYN);
|
||||
if (boxed.type.kind !== "dyn") return fence();
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Slots reached only through what a type assertion claims. */
|
||||
import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import { canDynCheckTo, DYN, isUnitType, typeEquals } from "../../../ir/ir.js";
|
||||
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { unionRetagHelper } from "./unions.js";
|
||||
|
||||
/** A value an erased assertion produced (`x as T`, lowerer.assertedValues)
|
||||
* flowing into a slot that no ordinary conversion reaches. Runs only where
|
||||
* the slot would otherwise refuse the value (SC2002/SC2003), so every
|
||||
* conversion that already lowered keeps its code.
|
||||
*
|
||||
* - A union whose arms only partly convert (`api as Partial<Record<string,
|
||||
* S>>` on `S | Partial<Record<K, S>> | undefined`): the arms that convert
|
||||
* into T convert into the slot, and an arm the assertion excludes (no
|
||||
* conversion into T) throws the catchable TypeError of a lying assertion.
|
||||
* - A record or array whose member types the assertion narrows (`models as
|
||||
* Model<"one">[]` on a union-element array): the checked deep conversion
|
||||
* a cast of an unknown value takes, which validates the value member by
|
||||
* member and names the deepest failing path. */
|
||||
export function assertedConversion(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType,
|
||||
): IrExpr | null {
|
||||
const asserted = lowerer.assertedValues.get(expr);
|
||||
if (!asserted) return null;
|
||||
const loc = expr.loc;
|
||||
if (expr.type.kind === "union") {
|
||||
const from = lowerer.unions.get(expr.type.unionId);
|
||||
if (!from) return null;
|
||||
const values = from.arms.filter((arm) => !isUnitType(arm));
|
||||
const kept = values.filter((arm) => convertsInto(lowerer, arm, asserted, loc));
|
||||
if (kept.length === 0 || kept.length === values.length) return null;
|
||||
if (asserted.kind === "union" && expected.kind === "union") {
|
||||
// The arms T excludes trap; the others (units included) re-tag.
|
||||
const trappable = new Set<number>();
|
||||
from.arms.forEach((arm, tag) => {
|
||||
if (!isUnitType(arm) && !kept.includes(arm)) trappable.add(tag);
|
||||
});
|
||||
const helper = unionRetagHelper(lowerer, from.id, expected.unionId, loc, trappable);
|
||||
return helper ? { kind: "call", callee: helper, args: [expr], type: expected, loc } : null;
|
||||
}
|
||||
// A record or array T holds one arm's values: extract that arm (every
|
||||
// other arm, a unit too, contradicts the assertion) and convert it.
|
||||
if (asserted.kind === "union" || kept.length !== 1) return null;
|
||||
const arm = kept[0]!;
|
||||
const helper = lowerer.narrowedArmHelper(from.id, arm, loc);
|
||||
if (!helper) return null;
|
||||
const narrowed = lowerer.checkedNarrowCall(helper, expr, arm, loc);
|
||||
if (typeEquals(arm, expected)) return narrowed;
|
||||
const converted = lowerer.coerceToExpected(narrowed, expected);
|
||||
return typeEquals(converted.type, expected) ? converted : null;
|
||||
}
|
||||
if (expected.kind !== "record" && expected.kind !== "array") return null;
|
||||
if (expected.kind === "record" && lowerer.shapes.get(expected.shapeId)?.indexValue !== undefined)
|
||||
return null;
|
||||
if (
|
||||
!lowerer.dynConvertible(expr.type) ||
|
||||
!canDynCheckTo(
|
||||
expected,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return { kind: "dynCheck", value: lowerer.coerceToExpected(expr, DYN), type: expected, loc };
|
||||
}
|
||||
|
||||
/** True when a value of `arm` has a structural conversion into `target`:
|
||||
* the same type, one of its arms, a width lift, or (into an
|
||||
* index-signature record) the overflow capture. */
|
||||
function convertsInto(lowerer: Lowerer, arm: IrType, target: IrType, loc: SrcLoc): boolean {
|
||||
if (typeEquals(arm, target)) return true;
|
||||
if (
|
||||
target.kind === "union" &&
|
||||
lowerer.unions.get(target.unionId)?.arms.some((candidate) => typeEquals(candidate, arm))
|
||||
)
|
||||
return true;
|
||||
if (lowerer.widthLiftPlan(arm, target) !== null) return true;
|
||||
return (
|
||||
arm.kind === "record" &&
|
||||
target.kind === "record" &&
|
||||
lowerer.shapes.get(target.shapeId)?.indexValue !== undefined &&
|
||||
lowerRecordOvfCaptureHelper(lowerer, arm.shapeId, target.shapeId, loc) !== null
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,14 @@ export class CoercionState {
|
||||
// The extracted arm of each checked (non-defaulting) extraction helper.
|
||||
readonly checkedNarrowArms = new Map<
|
||||
string,
|
||||
{ unionId: string; tag: number; target: IrType; widens?: true; message?: string }
|
||||
{
|
||||
unionId: string;
|
||||
tag: number;
|
||||
target: IrType;
|
||||
widens?: true;
|
||||
message?: string;
|
||||
origin?: string;
|
||||
}
|
||||
>();
|
||||
readonly retags = new Map<string, string>();
|
||||
readonly narrows = new Map<string, string>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fieldOrigin } from "../decl-origin.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { constructedValue } from "../constructor-returns.js";
|
||||
import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
@@ -394,7 +395,15 @@ export function recordWidthHelper(
|
||||
// orphan one).
|
||||
const plan = lowerer.recordWidthPlan(fromId, toId);
|
||||
if (!plan) return null;
|
||||
const key = `rec:${fromId}:${toId}`;
|
||||
// Package code converting into a package-declared record type (an
|
||||
// object literal returned under `@returns {HelpContext}`): its field
|
||||
// extractions name the declared fields (decl-origin.ts), so the helper
|
||||
// is keyed by that type's declaration too (two packages may declare
|
||||
// records of the same name and shape). Every other projection is shared
|
||||
// as before.
|
||||
const declared = lowerer.coercionRecordOrigin();
|
||||
const key =
|
||||
declared === null ? `rec:${fromId}:${toId}` : `rec:${fromId}:${toId}:origin:${declared.key}`;
|
||||
const existing = lowerer.valueHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%rec.width", key);
|
||||
@@ -402,7 +411,21 @@ export function recordWidthHelper(
|
||||
// (self-referential shapes) resolves to this helper itself.
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const fromT: IrType = { kind: "record", shapeId: fromId };
|
||||
lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc));
|
||||
const outerNode = lowerer.coercionOriginNode;
|
||||
const outerField = lowerer.coercionFieldOrigin;
|
||||
// Nested conversions see only their own field's origin.
|
||||
lowerer.coercionOriginNode = null;
|
||||
lowerer.coercionFieldOrigin = null;
|
||||
try {
|
||||
lowerer.liftedFns.push(
|
||||
buildRecordProjection(lowerer, name, fromT, to, plan, loc, (field) =>
|
||||
declared === null ? null : fieldOrigin(lowerer.checker, declared, field),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
lowerer.coercionOriginNode = outerNode;
|
||||
lowerer.coercionFieldOrigin = outerField;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -799,6 +822,7 @@ function buildRecordProjection(
|
||||
target: IrRecordShape,
|
||||
plan: Map<string, FieldLift>,
|
||||
loc: SrcLoc,
|
||||
originOf: (field: string) => string | null = () => null,
|
||||
): IrFunction {
|
||||
const parameter = source.kind === "record" ? "r" : "o";
|
||||
const localId = parameter + ".0";
|
||||
@@ -855,7 +879,12 @@ function buildRecordProjection(
|
||||
type: conversion.src,
|
||||
loc,
|
||||
};
|
||||
// Only the field's own extraction (a plain narrow) carries the
|
||||
// declared field's origin; nested conversions are shared.
|
||||
lowerer.coercionFieldOrigin =
|
||||
conversion.lift.how === "narrow" ? originOf(field.name) : null;
|
||||
const lifted = lowerer.applyWidthLift(conversion.lift, read, field.type, loc);
|
||||
lowerer.coercionFieldOrigin = null;
|
||||
return {
|
||||
name: field.name,
|
||||
value:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderOriginNote } from "../decl-origin.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js";
|
||||
import { isSafeToRepeat } from "../expressions/evaluation-safety.js";
|
||||
@@ -408,7 +409,24 @@ export function narrowedArmHelper(
|
||||
if (!from || isUnitType(target)) return null;
|
||||
const tag = lowerer.armTag(fromId, target);
|
||||
if (tag < 0) return null;
|
||||
const key = message === undefined ? `${fromId}:${tag}` : `${fromId}:${tag}:${message}`;
|
||||
// A slot in package code typed by a package declaration: the TypeError
|
||||
// names the declaration (decl-origin.ts). Other extractions keep their
|
||||
// shared helper.
|
||||
const originNode =
|
||||
message === undefined && lowerer.coercionOriginUnion === fromId
|
||||
? lowerer.coercionOriginNode
|
||||
: null;
|
||||
const origin =
|
||||
message !== undefined
|
||||
? null
|
||||
: (lowerer.coercionFieldOrigin ??
|
||||
(originNode === null ? null : lowerer.slotOriginOf(originNode)));
|
||||
const key =
|
||||
message !== undefined
|
||||
? `${fromId}:${tag}:${message}`
|
||||
: origin !== null
|
||||
? `${fromId}:${tag}:origin:${origin}`
|
||||
: `${fromId}:${tag}`;
|
||||
const existing = lowerer.coercions.narrows.get(key);
|
||||
if (existing) {
|
||||
lowerer.coercions.checkedNarrows.add(existing);
|
||||
@@ -425,6 +443,9 @@ export function narrowedArmHelper(
|
||||
(type) => lowerer.fmt(type),
|
||||
null,
|
||||
message ?? null,
|
||||
null,
|
||||
origin,
|
||||
lowerer.targetPlatform === "win32",
|
||||
);
|
||||
if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction");
|
||||
lowerer.coercions.checkedNarrowArms.set(name, {
|
||||
@@ -432,6 +453,7 @@ export function narrowedArmHelper(
|
||||
tag,
|
||||
target: from.arms[tag]!,
|
||||
...(message === undefined ? {} : { message }),
|
||||
...(origin === null ? {} : { origin }),
|
||||
});
|
||||
lowerer.liftedFns.push(fn);
|
||||
return name;
|
||||
@@ -516,8 +538,20 @@ export function checkedNarrowCall(
|
||||
let failure: IrExpr | null = null;
|
||||
for (let tag = from.arms.length - 1; tag >= 0; tag--) {
|
||||
if (tag === arm.tag) continue;
|
||||
const failing = from.arms[tag]!;
|
||||
const message =
|
||||
arm.message ?? unionNarrowFailureMessage(from.arms[tag]!, (t) => lowerer.fmt(t));
|
||||
(arm.message ?? unionNarrowFailureMessage(failing, (t) => lowerer.fmt(t))) +
|
||||
(arm.origin === undefined
|
||||
? ""
|
||||
: renderOriginNote(
|
||||
arm.origin,
|
||||
isUnitType(failing)
|
||||
? failing.kind === "undefinedT"
|
||||
? "undefined"
|
||||
: "null"
|
||||
: lowerer.fmt(failing),
|
||||
lowerer.targetPlatform === "win32",
|
||||
));
|
||||
const thrown = nodeThrowExpr(1, "", message, type, loc);
|
||||
failure =
|
||||
failure === null
|
||||
|
||||
@@ -233,6 +233,38 @@ function earlyHeritageReference(lowerer: Lowerer, binding: CycleEarlyBinding): t
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The identifiers of `sf` by text, each with its position in the file's
|
||||
* preorder walk: one walk per file serves every class's reference scan. */
|
||||
function fileIdentifiers(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
): ReadonlyMap<string, readonly { order: number; node: ts.Identifier }[]> {
|
||||
const memo = lowerer.program.analysis.fileIdentifiers;
|
||||
const known = memo.get(sf);
|
||||
if (known !== undefined) return known;
|
||||
const byText = new Map<string, { order: number; node: ts.Identifier }[]>();
|
||||
let order = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
order++;
|
||||
if (!ts.isIdentifier(node)) return undefined;
|
||||
const list = byText.get(node.text);
|
||||
if (list === undefined) byText.set(node.text, [{ order, node }]);
|
||||
else list.push({ order, node });
|
||||
return undefined;
|
||||
});
|
||||
memo.set(sf, byText);
|
||||
return byText;
|
||||
}
|
||||
|
||||
/** Whether `node` lies inside `ancestor` (or is it): the reference scans
|
||||
* skip the class's own declaration. */
|
||||
function within(node: ts.Node, ancestor: ts.Node | undefined): boolean {
|
||||
if (ancestor === undefined) return false;
|
||||
for (let current: ts.Node | undefined = node; current !== undefined; current = current.parent)
|
||||
if (current === ancestor) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether `node` names `symbol`, through import aliases when `aliases`. */
|
||||
function resolvesTo(
|
||||
lowerer: Lowerer,
|
||||
@@ -294,12 +326,14 @@ function classReferences(
|
||||
// the class's name or a local one.
|
||||
const names = classLocalNames(lowerer, sf, symbol, name.text, cycle);
|
||||
if (names.size === 0) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (node === declaration) return "skip";
|
||||
if (ts.isIdentifier(node) && names.has(node.text) && resolvesTo(lowerer, node, symbol, cycle))
|
||||
// The file's identifiers under those names, in walk order.
|
||||
const identifiers = fileIdentifiers(lowerer, sf);
|
||||
const named: { order: number; node: ts.Identifier }[] = [];
|
||||
for (const text of names) for (const entry of identifiers.get(text) ?? []) named.push(entry);
|
||||
if (names.size > 1) named.sort((a, b) => a.order - b.order);
|
||||
for (const { node } of named)
|
||||
if (!within(node, declaration) && resolvesTo(lowerer, node, symbol, cycle))
|
||||
out.push({ loc: locOf(node), name: node.text });
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
/* Origins of package-declared types at checked conversions.
|
||||
*
|
||||
* scriptc enforces declared types: a value that contradicts the type an
|
||||
* npm package declares (its .d.ts, @types package or JSDoc) throws a
|
||||
* TypeError where Node runs on. When the declaration is what is wrong, the
|
||||
* fix is a type patch (package.json scriptc.typePatches), so the error
|
||||
* names the declaration and the patch that would admit the value:
|
||||
*
|
||||
* TypeError: expected number at $, got undefined
|
||||
* termctx@1.2.0 declares Terminal.columns() as number (index.js:14)
|
||||
* if that declaration is wrong, patch it in package.json: "scriptc":
|
||||
* { "typePatches": { "termctx@^1.2.0": { "declarations":
|
||||
* { "Terminal.columns()": "| undefined" } } } }
|
||||
*
|
||||
* The origin is found for three kinds of slots: a value a package function
|
||||
* returns under a written return type, a property of an object literal
|
||||
* typed by a package-declared type, and a declaration-boundary cast (a
|
||||
* value user code receives from a package; decl-projection.ts records
|
||||
* those with originAt). Generic declarations get none: their types come
|
||||
* from how the program instantiates them. Checks in
|
||||
* code that names no package declaration carry no origin, so programs
|
||||
* without npm packages lower exactly as before.
|
||||
*
|
||||
* The origin string's fields (separated by \x1f) are read by the runtime
|
||||
* (scr_type_origin.c) for checked conversions, whose path and actual kind
|
||||
* are only known when they fail, and rendered here for union extractions,
|
||||
* whose failing arm is known statically. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { everyModuleNode } from "../../ir/traverse.js";
|
||||
import { trackedReadFile } from "../input-tracker.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
export const ORIGIN_SEPARATOR = "\x1f";
|
||||
|
||||
interface PackageFile {
|
||||
pkg: string;
|
||||
version: string;
|
||||
/** The file relative to the package root. */
|
||||
rel: string;
|
||||
}
|
||||
|
||||
/** Installed package versions by package root, read once per program load
|
||||
* (resetDeclOriginVersions): a dependency reinstalled at another version
|
||||
* between two builds of one process is read again. */
|
||||
const versions = new Map<string, string>();
|
||||
|
||||
export function resetDeclOriginVersions(): void {
|
||||
versions.clear();
|
||||
}
|
||||
|
||||
function packageFileOf(fileName: string): PackageFile | null {
|
||||
const path = fileName.split("\\").join("/");
|
||||
const marker = "/node_modules/";
|
||||
const at = path.lastIndexOf(marker);
|
||||
if (at < 0) return null;
|
||||
const parts = path.slice(at + marker.length).split("/");
|
||||
const nameParts = parts[0]!.startsWith("@") ? 2 : 1;
|
||||
if (parts.length <= nameParts) return null;
|
||||
let pkg = parts.slice(0, nameParts).join("/");
|
||||
const root = path.slice(0, at + marker.length) + pkg;
|
||||
let versionRoot = root;
|
||||
if (pkg.startsWith("@types/")) {
|
||||
// A twin describes the runtime package installed beside it.
|
||||
const bare = pkg.slice("@types/".length);
|
||||
const i = bare.indexOf("__");
|
||||
pkg = i > 0 ? `@${bare.slice(0, i)}/${bare.slice(i + 2)}` : bare;
|
||||
versionRoot = path.slice(0, at + marker.length) + pkg;
|
||||
}
|
||||
let version = versions.get(versionRoot);
|
||||
if (version === undefined) {
|
||||
version = "";
|
||||
try {
|
||||
const parsed = JSON.parse(trackedReadFile(`${versionRoot}/package.json`) ?? "{}") as {
|
||||
version?: unknown;
|
||||
};
|
||||
if (typeof parsed.version === "string") version = parsed.version;
|
||||
} catch {
|
||||
version = "";
|
||||
}
|
||||
versions.set(versionRoot, version);
|
||||
}
|
||||
return { pkg, version, rel: parts.slice(nameParts).join("/") };
|
||||
}
|
||||
|
||||
/** True for a node in an installed package's file. */
|
||||
export function inPackageFile(node: ts.Node): boolean {
|
||||
return packageFileOf(ts.getSourceFileOfNode(node).fileName) !== null;
|
||||
}
|
||||
|
||||
function lineOf(node: ts.Node): number {
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
return ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)).line + 1;
|
||||
}
|
||||
|
||||
function encode(file: PackageFile, selector: string, where: string, declared: string): string {
|
||||
const range = file.version === "" ? "*" : `^${file.version}`;
|
||||
return [file.pkg, file.version, range, selector, where, declared.replace(/\s+/g, " ")].join(
|
||||
ORIGIN_SEPARATOR,
|
||||
);
|
||||
}
|
||||
|
||||
function nameOf(name: ts.Node | undefined): string | null {
|
||||
if (name === undefined) return null;
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name))
|
||||
return name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `module.exports` → "export="; `exports.x` / `module.exports.x` → "x". */
|
||||
function exportTarget(left: ts.Node): string | null {
|
||||
if (!ts.isPropertyAccessExpression(left)) return null;
|
||||
const obj = left.expression;
|
||||
const prop = nameOf(left.name);
|
||||
if (prop === null) return null;
|
||||
if (ts.isIdentifier(obj) && obj.text === "module" && prop === "exports") return "export=";
|
||||
if (ts.isIdentifier(obj) && obj.text === "exports") return prop;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(obj) &&
|
||||
ts.isIdentifier(obj.expression) &&
|
||||
obj.expression.text === "module" &&
|
||||
nameOf(obj.name) === "exports"
|
||||
)
|
||||
return prop;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The selector naming a function-like declaration (without `()`), or
|
||||
* null when it has no name a selector can reach. */
|
||||
function functionSelector(fn: ts.Node): string | null {
|
||||
if (ts.isFunctionDeclaration(fn)) return nameOf(fn.name);
|
||||
if (ts.isMethodDeclaration(fn) || ts.isGetAccessor(fn)) {
|
||||
const method = nameOf(fn.name);
|
||||
const holder = fn.parent;
|
||||
if (method === null || holder === undefined) return null;
|
||||
if (ts.isClassDeclaration(holder) || ts.isClassExpression(holder)) {
|
||||
const cls = nameOf(holder.name);
|
||||
return cls === null ? null : `${cls}.${method}`;
|
||||
}
|
||||
if (ts.isObjectLiteralExpression(holder)) {
|
||||
const owner = valueSelector(holder);
|
||||
return owner === null ? null : `${owner}.${method}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isFunctionExpression(fn) || ts.isArrowFunction(fn)) return valueSelector(fn);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The selector of the declaration an expression is the value of. */
|
||||
function valueSelector(value: ts.Node): string | null {
|
||||
let e = value;
|
||||
while (e.parent !== undefined && ts.isParenthesizedExpression(e.parent)) e = e.parent;
|
||||
const p = e.parent;
|
||||
if (p === undefined) return null;
|
||||
if (ts.isVariableDeclaration(p) && p.initializer === e) return nameOf(p.name);
|
||||
if (
|
||||
ts.isBinaryExpression(p) &&
|
||||
p.right === e &&
|
||||
p.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
return exportTarget(p.left);
|
||||
if (ts.isPropertyAssignment(p) && p.initializer === e && p.parent !== undefined) {
|
||||
const name = nameOf(p.name);
|
||||
const owner = ts.isObjectLiteralExpression(p.parent) ? valueSelector(p.parent) : null;
|
||||
return name === null || owner === null ? null : `${owner}.${name}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True when a function-like declaration writes its return type: a type
|
||||
* annotation, or a JSDoc @returns tag. */
|
||||
function writesReturnType(fn: ts.Node): boolean {
|
||||
if ((fn as ts.FunctionDeclaration).type !== undefined) return true;
|
||||
const sf = ts.getSourceFileOfNode(fn);
|
||||
let owner: ts.Node = fn;
|
||||
// The JSDoc of `exports.f = function`, `const f = () =>` sits on the
|
||||
// statement.
|
||||
while (
|
||||
owner.parent !== undefined &&
|
||||
!ts.isSourceFile(owner.parent) &&
|
||||
!ts.isBlock(owner.parent) &&
|
||||
!ts.isClassDeclaration(owner.parent) &&
|
||||
!ts.isClassExpression(owner.parent) &&
|
||||
!ts.isObjectLiteralExpression(owner.parent)
|
||||
)
|
||||
owner = owner.parent;
|
||||
const leading = sf.text.slice(owner.pos, ts.tokenStartOfNode(owner));
|
||||
const doc = leading.lastIndexOf("/**");
|
||||
return doc >= 0 && /@returns?\s*\{/.test(leading.slice(doc));
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.Node | null {
|
||||
for (let p = node.parent; p !== undefined; p = p.parent) {
|
||||
if (
|
||||
ts.isFunctionDeclaration(p) ||
|
||||
ts.isFunctionExpression(p) ||
|
||||
ts.isArrowFunction(p) ||
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessor(p)
|
||||
)
|
||||
return p;
|
||||
if (ts.isSourceFile(p) || ts.isClassDeclaration(p)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function typeText(checker: ts.TypeChecker, type: ts.Type | undefined): string {
|
||||
if (type === undefined) return "";
|
||||
try {
|
||||
return checker.typeToString(type);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** The origin of the slot a value expression in a package file flows
|
||||
* into, or null when no package declaration types that slot. */
|
||||
export function slotOrigin(checker: ts.TypeChecker, node: ts.Node): string | null {
|
||||
let value = node;
|
||||
while (value.parent !== undefined && ts.isParenthesizedExpression(value.parent))
|
||||
value = value.parent;
|
||||
const parent = value.parent;
|
||||
if (parent === undefined) return null;
|
||||
// A property of an object literal typed by a declared type.
|
||||
if (
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === value) ||
|
||||
ts.isShorthandPropertyAssignment(value) ||
|
||||
(ts.isShorthandPropertyAssignment(parent) && parent.name === value)
|
||||
) {
|
||||
const property = ts.isShorthandPropertyAssignment(value) ? value : parent;
|
||||
const literal = property.parent;
|
||||
const name = nameOf((property as ts.PropertyAssignment).name);
|
||||
if (literal === undefined || !ts.isObjectLiteralExpression(literal) || name === null)
|
||||
return null;
|
||||
const contextual = checker.getContextualType(literal);
|
||||
if (contextual === undefined) return null;
|
||||
const typeSymbol = contextual.getAliasSymbol() ?? contextual.getSymbol();
|
||||
const typeName = typeSymbol === undefined ? "" : ts.symbolName(typeSymbol);
|
||||
if (!/^[A-Za-z_$][\w$]*$/.test(typeName)) return null;
|
||||
const symbol = checker.getPropertyOfType(contextual, name);
|
||||
const decl = symbol?.declarations[0];
|
||||
if (symbol === undefined || decl === undefined || genericDeclaration(decl)) return null;
|
||||
const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName);
|
||||
if (file === null) return null;
|
||||
return encode(
|
||||
file,
|
||||
`${typeName}.${name}`,
|
||||
`${file.rel}:${lineOf(decl)}`,
|
||||
typeText(checker, checker.getTypeOfSymbol(symbol)),
|
||||
);
|
||||
}
|
||||
// A value a package function returns under its written return type.
|
||||
let fn: ts.Node | null = null;
|
||||
if (ts.isReturnStatement(parent) && parent.expression === value) fn = enclosingFunction(parent);
|
||||
else if (ts.isArrowFunction(parent) && parent.body === value) fn = parent;
|
||||
if (fn === null || !writesReturnType(fn)) return null;
|
||||
const file = packageFileOf(ts.getSourceFileOfNode(fn).fileName);
|
||||
const selector = functionSelector(fn);
|
||||
if (file === null || selector === null) return null;
|
||||
let declared = "";
|
||||
try {
|
||||
const signature = checker.getSignatureFromDeclaration(fn as ts.SignatureDeclaration);
|
||||
declared =
|
||||
signature === undefined ? "" : typeText(checker, checker.getReturnTypeOfSignature(signature));
|
||||
} catch {
|
||||
declared = "";
|
||||
}
|
||||
return encode(file, `${selector}()`, `${file.rel}:${lineOf(fn)}`, declared);
|
||||
}
|
||||
|
||||
/** A package-declared record type a value is converted to (the
|
||||
* contextual type of a value in package code, such as an object literal
|
||||
* returned under `@returns {HelpContext}`), or null. */
|
||||
export interface RecordOrigin {
|
||||
type: ts.Type;
|
||||
name: string;
|
||||
/** The declaration's identity (package, version, file, line and name):
|
||||
* records of the same shape and name declared by two packages differ. */
|
||||
key: string;
|
||||
}
|
||||
|
||||
export function recordOrigin(checker: ts.TypeChecker, node: ts.Node): RecordOrigin | null {
|
||||
const contextual = checker.getContextualType(node);
|
||||
if (contextual === undefined) return null;
|
||||
const symbol = contextual.getAliasSymbol() ?? contextual.getSymbol();
|
||||
const name = symbol === undefined ? "" : ts.symbolName(symbol);
|
||||
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return null;
|
||||
const decl = symbol?.declarations[0];
|
||||
if (decl === undefined || genericDeclaration(decl)) return null;
|
||||
const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName);
|
||||
if (file === null) return null;
|
||||
const key = [file.pkg, file.version, file.rel, lineOf(decl), name].join(ORIGIN_SEPARATOR);
|
||||
return { type: contextual, name, key };
|
||||
}
|
||||
|
||||
/** The origin of one field of a package-declared record type. */
|
||||
export function fieldOrigin(
|
||||
checker: ts.TypeChecker,
|
||||
record: RecordOrigin,
|
||||
field: string,
|
||||
): string | null {
|
||||
const symbol = checker.getPropertyOfType(record.type, field);
|
||||
const decl = symbol?.declarations[0];
|
||||
if (symbol === undefined || decl === undefined || genericDeclaration(decl)) return null;
|
||||
const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName);
|
||||
if (file === null) return null;
|
||||
return encode(
|
||||
file,
|
||||
`${record.name}.${field}`,
|
||||
`${file.rel}:${lineOf(decl)}`,
|
||||
typeText(checker, checker.getTypeOfSymbol(symbol)),
|
||||
);
|
||||
}
|
||||
|
||||
/** The origin of a declaration in a package file named by `selector`. */
|
||||
export function originAt(decl: ts.Node, selector: string, declared: string): string | null {
|
||||
const file = packageFileOf(ts.getSourceFileOfNode(decl).fileName);
|
||||
if (file === null) return null;
|
||||
return encode(file, selector, `${file.rel}:${lineOf(decl)}`, declared);
|
||||
}
|
||||
|
||||
/** True when a declaration or the class, interface or type alias holding
|
||||
* it has type parameters: its declared type then comes from how the
|
||||
* program instantiates it (a lying `z.custom<T>()`), not from the package. */
|
||||
function genericDeclaration(decl: ts.Node): boolean {
|
||||
for (let n: ts.Node | undefined = decl; n !== undefined && !ts.isSourceFile(n); n = n.parent) {
|
||||
const params = (n as ts.FunctionDeclaration).typeParameters;
|
||||
if (params !== undefined && params.nodes.length > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The selector of a declaration in a declaration file: a function, a
|
||||
* member of a class, interface or named object type, or null (also for
|
||||
* generic declarations, whose types the program supplies). */
|
||||
export function declarationSelector(decl: ts.Node): string | null {
|
||||
if (genericDeclaration(decl)) return null;
|
||||
const own = nameOf((decl as ts.FunctionDeclaration).name);
|
||||
if (own === null) return null;
|
||||
if (ts.isFunctionDeclaration(decl) || ts.isVariableDeclaration(decl)) return own;
|
||||
let holder = decl.parent;
|
||||
if (holder !== undefined && ts.isTypeLiteralNode(holder)) holder = holder.parent;
|
||||
if (
|
||||
holder !== undefined &&
|
||||
(ts.isClassDeclaration(holder) ||
|
||||
ts.isInterfaceDeclaration(holder) ||
|
||||
ts.isTypeAliasDeclaration(holder))
|
||||
) {
|
||||
const owner = nameOf(holder.name);
|
||||
return owner === null ? null : `${owner}.${own}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One argument of the suggested `scriptc types patch` command, quoted so
|
||||
* the shell passes it as written (scr_type_origin.c quotes the same way):
|
||||
* bare when it has only characters no shell treats specially; otherwise,
|
||||
* for a POSIX shell, in single quotes with each `'` as `'\''`, and for
|
||||
* Windows (cmd and PowerShell), in double quotes when it has none of the
|
||||
* characters either shell expands, escapes or ends a string with there. Null when no quoting
|
||||
* is inert there: the note then names the command without arguments.
|
||||
* Package names, selectors and types come from package declarations. */
|
||||
export function shellArgument(value: string, windows: boolean): string | null {
|
||||
if (/^[A-Za-z0-9_@+=:,./-]+$/.test(value)) return value;
|
||||
if (!windows)
|
||||
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
||||
for (const ch of value) {
|
||||
const code = ch.codePointAt(0)!;
|
||||
// PowerShell also takes the typographic quotes U+2018-U+201F as quotes.
|
||||
if (code < 0x20 || code === 0x7f || (code >= 0x2018 && code <= 0x201f)) return null;
|
||||
if ('"%!^$`'.includes(ch)) return null;
|
||||
}
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
/** The suggested patch for a declaration: `scriptc types patch` with the
|
||||
* package, the selector and the type, each quoted for the target's shell. */
|
||||
export function patchSuggestion(
|
||||
pkg: string,
|
||||
selector: string,
|
||||
type: string,
|
||||
windows: boolean,
|
||||
): string {
|
||||
const args = [pkg, selector, type].map((a) => shellArgument(a, windows));
|
||||
return args.every((a) => a !== null)
|
||||
? `if that declaration is wrong, patch it: scriptc types patch ${args.join(" ")}`
|
||||
: "if that declaration is wrong, patch it with scriptc types patch";
|
||||
}
|
||||
|
||||
/** The note a union extraction's TypeError carries for a failing `arm`
|
||||
* (the same text the runtime renders for checked conversions). `windows`:
|
||||
* the program targets Windows, whose shells quote differently. */
|
||||
export function renderOriginNote(origin: string, arm: string, windows = false): string {
|
||||
const [pkg = "", version = "", , selector = "", where = "", declared = ""] =
|
||||
origin.split(ORIGIN_SEPARATOR);
|
||||
return (
|
||||
`\n ${pkg}${version === "" ? "" : `@${version}`} declares ${selector}` +
|
||||
`${declared === "" ? "" : ` as ${declared}`} (${where})` +
|
||||
`\n ${patchSuggestion(pkg, selector, `| ${arm}`, windows)}`
|
||||
);
|
||||
}
|
||||
|
||||
/** Keeps only the origins the module's checks still reference (a coercion
|
||||
* whose check was later replaced leaves its origin behind), renumbered in
|
||||
* first-use order; drops the table when none is left, so the runtime unit
|
||||
* is not linked for nothing. */
|
||||
export function compactDeclOrigins(mod: IrModule): void {
|
||||
const table = mod.declOrigins ?? [];
|
||||
const remap = new Map<number, number>();
|
||||
const used: string[] = [];
|
||||
// A node the IR shares at several places is renumbered once.
|
||||
const checks = new Set<IrExpr & { kind: "dynCheck" }>();
|
||||
everyModuleNode(mod, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "dynCheck" && e.origin !== undefined) {
|
||||
// An exact capsule check never throws: nothing to name.
|
||||
if (e.exact === true) {
|
||||
delete e.origin;
|
||||
return true;
|
||||
}
|
||||
checks.add(e);
|
||||
if (!remap.has(e.origin)) {
|
||||
remap.set(e.origin, used.length);
|
||||
used.push(table[e.origin]!);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
type: () => true,
|
||||
});
|
||||
for (const check of checks) check.origin = remap.get(check.origin!)!;
|
||||
if (used.length === 0) delete mod.declOrigins;
|
||||
else mod.declOrigins = used;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ import { lowerGlobalValue } from "../lower-global-value.js";
|
||||
import { fenceEnumObjectValue } from "../lower-enums.js";
|
||||
import { mixinFnOfCallee } from "../lower-mixins.js";
|
||||
import { isNavigatorGlobal, refuseNavigator } from "../builtins/navigator.js";
|
||||
import { wasiGuestPath } from "../../../wasi-paths.js";
|
||||
import { moduleFileName } from "../import-meta.js";
|
||||
|
||||
export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr {
|
||||
if (lowerer.isStdlibGlobal(expr, "console")) {
|
||||
@@ -1069,12 +1069,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
);
|
||||
}
|
||||
|
||||
function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
|
||||
return lowerer.targetPlatform === "wasi"
|
||||
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
|
||||
: sf.fileName;
|
||||
}
|
||||
|
||||
/** A computed-key access (`value[key]` after `typeof value === "object"`)
|
||||
* of a runtime-optional binding the checker narrowed to one record arm of a
|
||||
* wider union. The binding re-tags to that record or undefined, so the keyed
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
|
||||
const LISTENER_METHODS = new Set([
|
||||
"on",
|
||||
"once",
|
||||
"addListener",
|
||||
"prependListener",
|
||||
"prependOnceListener",
|
||||
]);
|
||||
|
||||
/** `<x>.on("message", …)` and the other listener registrations. */
|
||||
export function isMessageListenerCall(call: ts.CallExpression): boolean {
|
||||
const callee = call.expression;
|
||||
const event = call.arguments.nodes[0];
|
||||
return (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
LISTENER_METHODS.has(callee.name.text) &&
|
||||
event !== undefined &&
|
||||
ts.isStringLiteral(event) &&
|
||||
event.text === "message"
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Type narrowing of lowered values, conditional and as-expressions, and runtime-optional receiver reads. */
|
||||
import { declBoundaryOrigin } from "../../decl-projection.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, dynFallbackType } from "../lowerer.js";
|
||||
import {
|
||||
@@ -50,6 +51,7 @@ import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js";
|
||||
import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js";
|
||||
import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js";
|
||||
import { isDeclBoundaryTypeNode } from "../../decl-projection.js";
|
||||
import { isMessageListenerCall } from "./message-listeners.js";
|
||||
|
||||
/** `c ? a : b`. An explicit destination constructs each branch at that
|
||||
* layout before joining it. Without one, retain the expression's inferred
|
||||
@@ -1481,6 +1483,7 @@ export function lowerAsExpression(
|
||||
target?.kind === "record" &&
|
||||
inner.type.shapeId !== target.shapeId
|
||||
) {
|
||||
lowerer.assertedValues.set(inner, target);
|
||||
return lowerer.coerceInto(expr, inner, target);
|
||||
}
|
||||
// `u as Arm` on a UNION value is `u!`'s spelling with a named arm
|
||||
@@ -1515,6 +1518,15 @@ export function lowerAsExpression(
|
||||
const asserted = checkedClassUnionAssertion(lowerer, inner, target, locOf(expr));
|
||||
if (asserted) return asserted;
|
||||
}
|
||||
// The erased value still answers to the assertion: a slot it cannot
|
||||
// reach by an ordinary conversion converts it as asserted
|
||||
// (assertedConversion).
|
||||
if (
|
||||
target &&
|
||||
(target.kind === "union" || target.kind === "record" || target.kind === "array") &&
|
||||
(inner.type.kind === "union" || inner.type.kind === "array")
|
||||
)
|
||||
lowerer.assertedValues.set(inner, target);
|
||||
return inner; // erasure, unchanged
|
||||
}
|
||||
|
||||
@@ -1531,7 +1543,7 @@ export function lowerAsExpression(
|
||||
// The declaration-typed boundary (decl-projection.ts): an identity-
|
||||
// preserving checked conversion of a value package code produced.
|
||||
if (isDeclBoundaryTypeNode(expr.type)) {
|
||||
const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr));
|
||||
const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr), expr);
|
||||
if (adopted) return adopted;
|
||||
}
|
||||
// An all-`unknown`-fields record target (`err as { code?: unknown }`
|
||||
@@ -1575,7 +1587,7 @@ export function lowerAsExpression(
|
||||
// contains undefined, so casts over parse results keep failing on
|
||||
// non-string values with the usual path-annotated TypeError.
|
||||
if (target.kind === "union" && lowerer.dynConvertible(target)) {
|
||||
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
||||
return checkedDynCast(lowerer, expr, inner, target);
|
||||
}
|
||||
// Uint8Array targets: the checked-dynamic tree carries a bytes kind now (converted
|
||||
// stdin chunks) — the extraction validates the kind and copies out.
|
||||
@@ -1619,9 +1631,145 @@ export function lowerAsExpression(
|
||||
`number, string, boolean, records, arrays, and unions of those)`,
|
||||
);
|
||||
}
|
||||
return checkedDynCast(lowerer, expr, inner, target);
|
||||
}
|
||||
|
||||
/** The checked conversion of a dynamic value. A record or array built from
|
||||
* a value another holder may still reference keeps one identity with it
|
||||
* (adoptingCast); only a source nothing else can hold (freshDynSource)
|
||||
* converts outright, which keeps the fused `JSON.parse(text) as T`. */
|
||||
function checkedDynCast(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AsExpression | ts.TypeAssertion,
|
||||
inner: IrExpr,
|
||||
target: IrType,
|
||||
): IrExpr {
|
||||
if (
|
||||
holdsReference(lowerer, target) &&
|
||||
!freshDynSource(lowerer, expr, inner) &&
|
||||
!resultOnlyRead(lowerer, expr)
|
||||
) {
|
||||
return adoptingCast(lowerer, inner, target, locOf(expr), "dyn.adoptShared");
|
||||
}
|
||||
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
||||
}
|
||||
|
||||
/** A cast whose result only serves one read of a primitive member
|
||||
* (`(value as T).name`, `(value as T[]).length`): the converted value is
|
||||
* never stored, compared, written or called through, and the read yields no
|
||||
* reference into it, so whether it is the source itself or a copy cannot be
|
||||
* observed. */
|
||||
function resultOnlyRead(lowerer: Lowerer, expr: ts.AsExpression | ts.TypeAssertion): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const access = node.parent;
|
||||
if (
|
||||
access === undefined ||
|
||||
!(
|
||||
(ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
|
||||
access.expression === node
|
||||
)
|
||||
)
|
||||
return false;
|
||||
const primitive =
|
||||
ts.TypeFlags.StringLike |
|
||||
ts.TypeFlags.NumberLike |
|
||||
ts.TypeFlags.BooleanLike |
|
||||
ts.TypeFlags.BigIntLike |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Null;
|
||||
const read = lowerer.typeOf(access);
|
||||
const parts = read.isUnionType() ? ts.constituentTypes(read) : [read];
|
||||
if (!parts.every((part) => (part.flags & primitive) !== 0)) return false;
|
||||
let use: ts.Node = access;
|
||||
while (use.parent !== undefined && ts.isParenthesizedExpression(use.parent)) use = use.parent;
|
||||
const parent = use.parent;
|
||||
if (parent === undefined) return true;
|
||||
if (ts.isCallExpression(parent) && parent.expression === use) return false;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === use &&
|
||||
ts.isAssignmentOperator(parent.operatorToken.kind)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
|
||||
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
parent.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
return false;
|
||||
if (ts.isDeleteExpression(parent)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A cast operand nothing else can hold: a value this expression creates
|
||||
* (`JSON.parse(text)`, `await response.json()`), or a structured clone the
|
||||
* runtime delivers once and the program reads at this site only (`workerData`
|
||||
* read nowhere else; the parameter of the program's one `"message"`
|
||||
* listener, read nowhere else in it). Anything else is adopted. */
|
||||
function freshDynSource(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AsExpression | ts.TypeAssertion,
|
||||
inner: IrExpr,
|
||||
): boolean {
|
||||
if (inner.kind === "libCall" && inner.fn === "json.parse") return true;
|
||||
if (
|
||||
inner.kind === "awaitExpr" &&
|
||||
inner.value.kind === "libCall" &&
|
||||
inner.value.fn === "fetch.responseJson"
|
||||
)
|
||||
return true;
|
||||
const facts = lowerer.structuredCloneReads();
|
||||
if (inner.kind === "libCall" && inner.fn === "worker.data") return facts.workerData === 1;
|
||||
let operand: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
if (!ts.isIdentifier(operand) || inner.kind !== "varRef" || facts.messageListeners !== 1)
|
||||
return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(operand);
|
||||
const param = symbol?.valueDeclaration;
|
||||
if (!param || !ts.isParameter(param) || !ts.isIdentifier(param.name)) return false;
|
||||
const fn = param.parent;
|
||||
if (
|
||||
!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) ||
|
||||
fn.parameters.nodes[0] !== param ||
|
||||
!isMessageListener(fn)
|
||||
)
|
||||
return false;
|
||||
let reads = 0;
|
||||
const name = param.name.text;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isIdentifier(node) && node.text === name && node !== param.name) reads++;
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
ts.forEachChild(fn, visit);
|
||||
return reads === 1;
|
||||
}
|
||||
|
||||
/** The listener argument of `<x>.on("message", fn)` (also once,
|
||||
* addListener, prependListener, prependOnceListener) registered once: at
|
||||
* module level, outside any function, class or loop, so one emitter can
|
||||
* never hand the same message to two registrations of it. */
|
||||
function isMessageListener(fn: ts.Node): boolean {
|
||||
const call = fn.parent;
|
||||
if (!call || !ts.isCallExpression(call) || call.arguments.nodes[1] !== fn) return false;
|
||||
if (!isMessageListenerCall(call)) return false;
|
||||
for (let p = call.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (
|
||||
ts.isFunctionLike(p) ||
|
||||
ts.isClassDeclaration(p) ||
|
||||
ts.isClassExpression(p) ||
|
||||
ts.isClassStaticBlockDeclaration(p) ||
|
||||
ts.isForStatement(p) ||
|
||||
ts.isForInStatement(p) ||
|
||||
ts.isForOfStatement(p) ||
|
||||
ts.isWhileStatement(p) ||
|
||||
ts.isDoStatement(p)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True when a type holds a record or an array somewhere (values whose
|
||||
* identity can be observed). */
|
||||
function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set<string>()): boolean {
|
||||
@@ -1639,11 +1787,42 @@ function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set<string>()):
|
||||
* becomes a live capsule of the value built from it
|
||||
* (scr_dyn_decl_adopt), so identity and mutation visibility hold on both
|
||||
* sides. Scalar targets are ordinary checked casts. */
|
||||
/** The origin the projection recorded for a boundary cast (the package
|
||||
* declaration its type comes from, decl-projection.ts), found by the
|
||||
* cast's position among the file's boundary casts in source order. */
|
||||
function recordedBoundaryOrigin(lowerer: Lowerer, cast: ts.Node): string | null {
|
||||
const sf = ts.getSourceFileOfNode(cast);
|
||||
let order = lowerer.boundaryCastOrder.get(sf);
|
||||
if (order === undefined) {
|
||||
const found = new Map<ts.Node, number>();
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isAsExpression(node) && isDeclBoundaryTypeNode(node.type)) found.set(node, found.size);
|
||||
ts.forEachChild(
|
||||
node,
|
||||
(child) => {
|
||||
visit(child);
|
||||
return undefined;
|
||||
},
|
||||
(list) => {
|
||||
for (const child of list.nodes) visit(child);
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
};
|
||||
visit(sf);
|
||||
order = found;
|
||||
lowerer.boundaryCastOrder.set(sf, order);
|
||||
}
|
||||
const index = order.get(cast);
|
||||
return index === undefined ? null : declBoundaryOrigin(sf.fileName, index);
|
||||
}
|
||||
|
||||
function declBoundaryCast(
|
||||
lowerer: Lowerer,
|
||||
inner: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
cast: ts.Node,
|
||||
): IrExpr | null {
|
||||
if (
|
||||
!canDynCheckTo(
|
||||
@@ -1654,9 +1833,42 @@ function declBoundaryCast(
|
||||
!(target.kind === "union" && lowerer.dynConvertible(target))
|
||||
)
|
||||
return null;
|
||||
if (!holdsReference(lowerer, target)) {
|
||||
return { kind: "dynCheck", value: inner, type: target, loc };
|
||||
// The package call or read the value comes from (decl-origin.ts): a
|
||||
// failing check names it and the patch admitting the value.
|
||||
let originText: string | null = null;
|
||||
try {
|
||||
originText = recordedBoundaryOrigin(lowerer, cast);
|
||||
} catch {
|
||||
originText = null;
|
||||
}
|
||||
const originId = originText === null ? undefined : lowerer.declOriginIndex(originText);
|
||||
if (!holdsReference(lowerer, target)) {
|
||||
return {
|
||||
kind: "dynCheck",
|
||||
value: inner,
|
||||
type: target,
|
||||
loc,
|
||||
...(originId === undefined ? {} : { origin: originId }),
|
||||
};
|
||||
}
|
||||
return adoptingCast(lowerer, inner, target, loc, "dyn.declAdopt", originId);
|
||||
}
|
||||
|
||||
/** The checked conversion of `inner` to `target` followed by adoption
|
||||
* (`dyn.declAdopt` at the boundary, the lenient `dyn.adoptShared`
|
||||
* elsewhere): every source object or array that something else still
|
||||
* references becomes a live capsule of the native value built from it.
|
||||
* `originId` names the package declaration a failing check reports
|
||||
* (decl-origin.ts). */
|
||||
function adoptingCast(
|
||||
lowerer: Lowerer,
|
||||
inner: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
fn: "dyn.declAdopt" | "dyn.adoptShared",
|
||||
originId?: number,
|
||||
): IrExpr {
|
||||
const boundary = fn === "dyn.declAdopt";
|
||||
const source = lowerer.declareHiddenLocal("%boundarySource", DYN);
|
||||
const value = lowerer.declareHiddenLocal("%boundaryValue", target);
|
||||
return {
|
||||
@@ -1669,9 +1881,10 @@ function declBoundaryCast(
|
||||
init: {
|
||||
kind: "dynCheck",
|
||||
value: varRef(source.id, DYN, loc),
|
||||
adopt: true,
|
||||
...(boundary ? { adopt: true as const } : {}),
|
||||
type: target,
|
||||
loc,
|
||||
...(originId === undefined ? {} : { origin: originId }),
|
||||
},
|
||||
loc,
|
||||
},
|
||||
@@ -1679,7 +1892,7 @@ function declBoundaryCast(
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.declAdopt",
|
||||
fn,
|
||||
args: [
|
||||
varRef(source.id, DYN, loc),
|
||||
{
|
||||
|
||||
@@ -419,7 +419,8 @@ export function lowerDynObjectLiteral(
|
||||
: !boxValue &&
|
||||
(ts.isObjectLiteralExpression(valueExpr) ||
|
||||
(ts.isArrayLiteralExpression(valueExpr) &&
|
||||
holdsEvolvingArray(lowerer, valueExpr)))
|
||||
(holdsEvolvingArray(lowerer, valueExpr) ||
|
||||
lowerer.untypedLiteralContext(valueExpr))))
|
||||
? lowerer.lowerExprExpecting(valueExpr, DYN)
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
|
||||
@@ -2529,6 +2530,20 @@ export function lowerObjectLiteral(
|
||||
: { kind: "seqExpr", stmts: spreadStmts, result, type, loc };
|
||||
}
|
||||
|
||||
/** The field type is an array, or a union with exactly one array arm. */
|
||||
function singleArrayArm(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "array") return true;
|
||||
if (type.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
return arms.filter((arm) => arm.kind === "array").length === 1;
|
||||
}
|
||||
|
||||
function isEmptyArrayLiteral(node: ts.Node): boolean {
|
||||
let inner = node;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
return ts.isArrayLiteralExpression(inner) && inner.elements.nodes.length === 0;
|
||||
}
|
||||
|
||||
/** A union of plain records can preserve its existing variant while
|
||||
* replacing shared fields. Dispatch before constructing the copy: only
|
||||
* the selected arm evaluates overrides, and recordClone snapshots fields
|
||||
@@ -2577,8 +2592,11 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
|
||||
for (let index = 0; index < props.length; index++) {
|
||||
const prop = props[index]!;
|
||||
const node = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
|
||||
const valueType = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
const fieldType = shape.fields.find((field) => field.name === names[index])!.type;
|
||||
// An empty array literal (`content: []`, typed `never[]`) builds a
|
||||
// fresh array of each arm's own element type.
|
||||
if (isEmptyArrayLiteral(node) && singleArrayArm(lowerer, fieldType)) continue;
|
||||
const valueType = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
const lift = valueType ? lowerer.widthLiftPlan(valueType, fieldType) : null;
|
||||
// The inferred result must retain an override's own fields and
|
||||
// aliases. A structural copy into the source field's narrower shape
|
||||
|
||||
@@ -156,7 +156,10 @@ 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 {
|
||||
createRequireResolveFenceValue,
|
||||
createRequireResolveValue,
|
||||
} from "../builtins/module-resolution.js";
|
||||
import {
|
||||
builtinPrototypeMethod,
|
||||
lowerArrayIsArrayValue,
|
||||
@@ -307,6 +310,7 @@ function isCreateRequireResolveValue(lowerer: Lowerer, expr: ts.PropertyAccessEx
|
||||
)
|
||||
resolve = expr.expression;
|
||||
return (
|
||||
!expr.questionDotToken &&
|
||||
ts.isPropertyAccessExpression(resolve) &&
|
||||
resolve.name.text === "resolve" &&
|
||||
createRequireCalleeFileOf(lowerer, resolve.expression) !== null
|
||||
@@ -1352,10 +1356,13 @@ export function lowerPropertyAccessExpr(
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
// picked by `??`): a function that resolves its request at run time.
|
||||
// resolve.paths read as a value has no run-time lowering: a function
|
||||
// that throws the fence when called.
|
||||
if (isCreateRequireResolveValue(lowerer, expr))
|
||||
return createRequireResolveFenceValue(lowerer, expr, loc);
|
||||
return expr.name.text === "paths"
|
||||
? createRequireResolveFenceValue(lowerer, expr, loc)
|
||||
: createRequireResolveValue(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.
|
||||
|
||||
@@ -17,14 +17,19 @@ export function isImportMeta(expr: ts.Expression): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The module's file name as the compiled program reports it
|
||||
* (import.meta.filename): the WASI guest path on wasi targets. */
|
||||
export function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
|
||||
return lowerer.targetPlatform === "wasi"
|
||||
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
|
||||
: sf.fileName;
|
||||
}
|
||||
|
||||
/** File-backed metadata is fixed by the compiled module graph. Reads share
|
||||
* one implementation across dot access, folded keys and selected bindings. */
|
||||
export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr {
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
const fileName =
|
||||
lowerer.targetPlatform === "wasi"
|
||||
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
|
||||
: sf.fileName;
|
||||
const fileName = moduleFileName(lowerer, sf);
|
||||
const loc = locOf(node);
|
||||
switch (name) {
|
||||
case "env":
|
||||
|
||||
@@ -10107,7 +10107,13 @@ function lowerObjLitGenericMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return lowerer.coerceInto(call, invoked, lowerer.irTypeOf(call));
|
||||
const resultType = lowerer.irTypeOf(call);
|
||||
// A void method's result stays the runtime's untyped value, as a
|
||||
// call through a non-generic void member's dispatch keeps it: a class
|
||||
// may implement the member with a method that returns a value, which
|
||||
// JavaScript passes on (`console.log(c.publish(m))`).
|
||||
if (resultType.kind === "void") return invoked;
|
||||
return lowerer.coerceInto(call, invoked, resultType);
|
||||
}
|
||||
}
|
||||
if (onInterface) {
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
||||
import { declarationGroupKey } from "./type-failure.js";
|
||||
import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
@@ -2306,6 +2307,10 @@ export function collectClassShapeInner(
|
||||
`extending classes not declared in the program ('${t.expression.text}')`,
|
||||
);
|
||||
lowerer.retriableDiags.add(diag);
|
||||
// A base the program declares but rejected: this rejection is its
|
||||
// cascade (localized failure, report-only).
|
||||
const rejectedBase = rejectedProgramClassGroup(lowerer, symbol);
|
||||
if (rejectedBase !== null) diag.cause = [rejectedBase];
|
||||
lowerer.pushDiag(diag);
|
||||
throw new PoisonError();
|
||||
}
|
||||
@@ -11865,3 +11870,14 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The failure group of a class the program declares but did not collect
|
||||
* (its own declaration was rejected), or null for any other symbol. */
|
||||
function rejectedProgramClassGroup(lowerer: Lowerer, symbol: ts.Symbol | null): string | null {
|
||||
if (symbol === null) return null;
|
||||
const recorded = lowerer.rejectedGroups.get(symbol);
|
||||
if (recorded !== undefined) return recorded;
|
||||
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
|
||||
if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
|
||||
return declarationGroupKey(symbol);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user