mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Merge main into Effect static compilation support
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
name: Pinned Linux LLVM
|
||||
description: Reuse the pinned LLVM development distribution across CI jobs
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
id: llvm
|
||||
with:
|
||||
path: ${{ runner.temp }}/llvm-22.1.8
|
||||
key: llvm-22.1.8-linux-x64-v1
|
||||
- name: Install LLVM system libraries
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes g++ zlib1g-dev libzstd-dev
|
||||
- name: Download LLVM
|
||||
if: steps.llvm.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-Linux-X64.tar.xz
|
||||
tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP"
|
||||
mv "$RUNNER_TEMP/LLVM-22.1.8-Linux-X64" "$RUNNER_TEMP/llvm-22.1.8"
|
||||
- name: Select LLVM
|
||||
shell: bash
|
||||
run: |
|
||||
echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV"
|
||||
echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH"
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Native compiler packages
|
||||
description: Build or restore native packages using their complete source inputs
|
||||
inputs:
|
||||
target:
|
||||
description: Native package suffix
|
||||
required: true
|
||||
helper:
|
||||
description: Build the LLVM helper as well as the runtime pack
|
||||
default: "true"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Identify host toolchain
|
||||
id: host
|
||||
shell: bash
|
||||
run: |
|
||||
# Runner image changes can update the SDK, C++ ABI, and C compiler.
|
||||
echo "image=${ImageOS}-${ImageVersion}" >> "$GITHUB_OUTPUT"
|
||||
if test "$RUNNER_OS" = Linux; then
|
||||
echo "zig=$(zig version)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- uses: actions/cache@v4
|
||||
id: helper
|
||||
if: inputs.helper == 'true'
|
||||
with:
|
||||
path: packages/llvm-${{ inputs.target }}/bin
|
||||
key: helper-v1-${{ inputs.target }}-${{ steps.host.outputs.image }}-${{ steps.host.outputs.zig }}-${{ hashFiles('.node-version', '.github/actions/native-packages/action.yml', '.github/actions/llvm-linux/action.yml', 'native/llvm-codegen/**', 'packages/llvm-*/package.json', 'packages/llvm-*/scripts/**') }}
|
||||
- uses: actions/cache@v4
|
||||
id: runtime
|
||||
with:
|
||||
path: |
|
||||
packages/runtime-${{ inputs.target }}/artifacts
|
||||
packages/runtime-${{ inputs.target }}/runtime-pack.json
|
||||
key: runtime-v1-${{ inputs.target }}-${{ steps.host.outputs.image }}-${{ steps.host.outputs.zig }}-${{ hashFiles('.node-version', '.github/actions/native-packages/action.yml', '.github/actions/llvm-linux/action.yml', 'packages/runtime/package.json', 'packages/runtime/LICENSE', 'packages/runtime/src/**', 'packages/runtime/vendor/**', 'packages/runtime-*/package.json', 'packages/runtime-*/scripts/**', 'packages/runtime-*/*.mjs') }}
|
||||
- name: Install macOS LLVM
|
||||
if: runner.os == 'macOS' && inputs.helper == 'true' && steps.helper.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: brew install llvm@22
|
||||
- name: Build LLVM helper
|
||||
if: inputs.helper == 'true' && steps.helper.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
SCRIPTC_PACKAGE_TARGET: ${{ inputs.target }}
|
||||
run: pnpm --filter "@scriptc/llvm-$SCRIPTC_PACKAGE_TARGET" build:native
|
||||
- name: Build runtime pack
|
||||
if: steps.runtime.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
SCRIPTC_PACKAGE_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
if test "$RUNNER_OS" = Linux; then
|
||||
CC=zig AR=zig pnpm --filter "@scriptc/runtime-$SCRIPTC_PACKAGE_TARGET" build:native
|
||||
else
|
||||
pnpm --filter "@scriptc/runtime-$SCRIPTC_PACKAGE_TARGET" build:native
|
||||
fi
|
||||
- name: Verify runtime content
|
||||
shell: bash
|
||||
env:
|
||||
SCRIPTC_PACKAGE_TARGET: ${{ inputs.target }}
|
||||
run: node "packages/runtime-$SCRIPTC_PACKAGE_TARGET/scripts/verify.mjs"
|
||||
+77
-47
@@ -10,7 +10,36 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
native_macos:
|
||||
name: build (macOS arm64 native packages)
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: darwin-arm64
|
||||
- name: Preserve executable permissions in shared packages
|
||||
run: >-
|
||||
tar -czf "$RUNNER_TEMP/native-darwin-arm64.tar.gz"
|
||||
packages/llvm-darwin-arm64/bin
|
||||
packages/runtime-darwin-arm64/artifacts
|
||||
packages/runtime-darwin-arm64/runtime-pack.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-darwin-arm64
|
||||
path: ${{ runner.temp }}/native-darwin-arm64.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
tests:
|
||||
needs: native_macos
|
||||
name: test (${{ matrix.flavor }}, ${{ matrix.shard }}/10)
|
||||
# arm64 macOS: same clang/libc the differential + number-formatting
|
||||
# oracles were verified against.
|
||||
@@ -45,10 +74,12 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: brew install llvm@22
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/llvm-darwin-arm64 build:native
|
||||
- run: pnpm --filter @scriptc/runtime-darwin-arm64 build:native
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-darwin-arm64
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz"
|
||||
- run: pnpm build
|
||||
# Separate vitest invocations because the shard axes must not mix: a file
|
||||
# lands in exactly ONE --shard slice, so an env-sharded file behind
|
||||
@@ -68,7 +99,6 @@ jobs:
|
||||
--exclude=tests/harness/self-hosting.test.ts
|
||||
--exclude=tests/harness/self-hosting-llvm-emitter.test.ts
|
||||
--exclude=tests/harness/self-hosting-native-driver.test.ts
|
||||
--exclude=tests/harness/self-hosting-bootstrap.test.ts
|
||||
--exclude=tests/harness/fetch.test.ts
|
||||
--exclude=tests/harness/npm-static.test.ts
|
||||
--exclude=tests/harness/surface-manifest.test.ts
|
||||
@@ -137,7 +167,7 @@ jobs:
|
||||
# Full compiler rebuilds dominate the critical path. Start them immediately
|
||||
# after setup, independently of corpus and integration test shards.
|
||||
bootstrap:
|
||||
name: test (Linux, ${{ matrix.flavor }}, ${{ matrix.compiler }} bootstrap)
|
||||
name: test (Linux, ${{ matrix.flavor }}, native bootstrap)
|
||||
# These contracts are portable. Standard Linux runners have enough memory
|
||||
# for LLVM optimization alongside the compiler that produces its input.
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -145,12 +175,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [plain, san]
|
||||
compiler: [frontend, native]
|
||||
include:
|
||||
- compiler: frontend
|
||||
test: tests/harness/self-hosting-bootstrap.test.ts
|
||||
- compiler: native
|
||||
test: tests/harness/self-hosting-native-driver.test.ts
|
||||
env:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
|
||||
@@ -163,23 +187,20 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- name: Install pinned LLVM development distribution
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes g++ zlib1g-dev libzstd-dev
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-Linux-X64.tar.xz
|
||||
tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP"
|
||||
mv "$RUNNER_TEMP/LLVM-22.1.8-Linux-X64" "$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"
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/llvm-linux-x64-gnu build:native
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: CC=zig AR=zig pnpm --filter @scriptc/runtime-linux-x64-gnu build:native
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: linux-x64-gnu
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: wasm32-wasi
|
||||
helper: "false"
|
||||
- run: pnpm build
|
||||
- run: pnpm test ${{ matrix.test }}
|
||||
- run: pnpm test tests/harness/self-hosting-native-driver.test.ts
|
||||
|
||||
# A real host-clang/glibc build, not the zig cross-target lane: catches
|
||||
# Linux feature-test macro omissions at compile time and separate-libm
|
||||
@@ -203,10 +224,11 @@ jobs:
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Host clang static-build regression
|
||||
- name: Host clang and sanitizer build regressions
|
||||
run: >-
|
||||
pnpm test packages/compiler/test/cc-driver.test.ts
|
||||
--testNamePattern "host-native clang static build"
|
||||
packages/compiler/src/native/sanitizer.test.ts
|
||||
--testNamePattern "host-native clang static build|sanitized vendor archives"
|
||||
|
||||
# The TS7 parity sweep exercises the compiler frontend, so it can run
|
||||
# alongside the Linux LLVM/runtime package build instead of following it.
|
||||
@@ -241,21 +263,14 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- name: Install pinned LLVM development distribution
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes g++ zlib1g-dev libzstd-dev
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/LLVM-22.1.8-Linux-X64.tar.xz
|
||||
tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP"
|
||||
mv "$RUNNER_TEMP/LLVM-22.1.8-Linux-X64" "$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"
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/llvm-linux-x64-gnu build:native
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: CC=zig AR=zig pnpm --filter @scriptc/runtime-linux-x64-gnu build:native
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: linux-x64-gnu
|
||||
- name: Build and verify Linux x64 musl release packages
|
||||
run: |
|
||||
pnpm --filter @scriptc/llvm-linux-x64-musl build:native
|
||||
@@ -272,7 +287,10 @@ jobs:
|
||||
if (value.default_target !== "x86_64-unknown-linux-musl" || !value.supported_targets?.includes("wasm32-unknown-wasi")) process.exit(1);
|
||||
'
|
||||
node packages/runtime-linux-x64-musl/scripts/verify.mjs
|
||||
- run: pnpm --filter @scriptc/runtime-wasm32-wasi build:native
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: wasm32-wasi
|
||||
helper: "false"
|
||||
- run: pnpm build
|
||||
- name: Linux helper object format and no-clang output contract
|
||||
run: |
|
||||
@@ -288,6 +306,8 @@ jobs:
|
||||
run: |
|
||||
node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o "$RUNNER_TEMP/hello"
|
||||
test "$("$RUNNER_TEMP/hello")" = 'hello world'
|
||||
- name: Linux release runtime reachability and debug payload
|
||||
run: pnpm test tests/harness/runtime-tree-shaking.test.ts
|
||||
- name: Debian 12 glibc 2.36 runtime-pack smoke
|
||||
run: |
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" --workdir /work node:24-bookworm-slim sh -ec '
|
||||
@@ -297,9 +317,10 @@ jobs:
|
||||
test "$(/tmp/hello)" = "hello world"
|
||||
'
|
||||
- name: WASI helper object/runtime-pack smoke
|
||||
run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts
|
||||
run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts tests/harness/wasm-library.test.ts
|
||||
|
||||
llvm_artifacts_macos_arm64:
|
||||
needs: native_macos
|
||||
name: test (macOS arm64 LLVM artifacts, no clang, ${{ matrix.shard }}/5)
|
||||
runs-on: macos-15
|
||||
# Shard 1 additionally runs package/install and link-parity contracts
|
||||
@@ -325,10 +346,12 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: brew install llvm@22
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/llvm-darwin-arm64 build:native
|
||||
- run: pnpm --filter @scriptc/runtime-darwin-arm64 build:native
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-darwin-arm64
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz"
|
||||
- run: pnpm build
|
||||
- name: No-clang assembly/object contract
|
||||
if: matrix.shard == 1
|
||||
@@ -353,22 +376,29 @@ jobs:
|
||||
node scripts/verify-llvm-package.mjs "$RUNNER_TEMP/$(basename "$TARBALL")"
|
||||
- name: Packed npm installation smoke
|
||||
if: matrix.shard == 1
|
||||
env:
|
||||
# Release optimization is covered by the compiler bootstrap jobs.
|
||||
# This lane verifies packaging and installation with a faster seed.
|
||||
SCRIPTC_NATIVE_OPTIMIZATION: dev
|
||||
run: |
|
||||
pnpm --filter @scriptc/cli-darwin-arm64 build:native
|
||||
pnpm --dir packages/runtime pack --pack-destination "$RUNNER_TEMP" --silent
|
||||
pnpm --dir packages/runtime-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent
|
||||
pnpm --dir packages/compiler pack --pack-destination "$RUNNER_TEMP" --silent
|
||||
pnpm --dir packages/cli-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent
|
||||
pnpm --dir packages/cli pack --pack-destination "$RUNNER_TEMP" --silent
|
||||
PREFIX="$RUNNER_TEMP/installed-scriptc"
|
||||
npm install --prefix "$PREFIX" --ignore-scripts \
|
||||
npm install --prefix "$PREFIX" --no-audit --no-fund \
|
||||
"$RUNNER_TEMP/scriptc-runtime-$(node -p "require('./packages/runtime/package.json').version").tgz" \
|
||||
"$RUNNER_TEMP/scriptc-runtime-darwin-arm64-$(node -p "require('./packages/runtime-darwin-arm64/package.json').version").tgz" \
|
||||
"$RUNNER_TEMP/scriptc-llvm-darwin-arm64-$(node -p "require('./packages/llvm-darwin-arm64/package.json').version").tgz" \
|
||||
"$RUNNER_TEMP/scriptc-compiler-$(node -p "require('./packages/compiler/package.json').version").tgz" \
|
||||
"$RUNNER_TEMP/scriptc-cli-darwin-arm64-$(node -p "require('./packages/cli-darwin-arm64/package.json').version").tgz" \
|
||||
"$RUNNER_TEMP/scriptc-$(node -p "require('./packages/cli/package.json').version").tgz"
|
||||
"$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
|
||||
PATH="" "$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
|
||||
--emit=obj -o "$RUNNER_TEMP/installed.o"
|
||||
file "$RUNNER_TEMP/installed.o" | grep 'Mach-O 64-bit object arm64'
|
||||
"$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
|
||||
PATH="" "$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
|
||||
--print=native-link-info -o "$RUNNER_TEMP/installed-link.o" \
|
||||
> "$RUNNER_TEMP/installed-link.json"
|
||||
node examples/native-object/link.mjs cc \
|
||||
@@ -388,10 +418,10 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: brew install llvm@22
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: LLVM_DIR=/usr/local/opt/llvm@22/lib/cmake/llvm pnpm --filter @scriptc/llvm-darwin-x64 build:native
|
||||
- run: pnpm --filter @scriptc/runtime-darwin-x64 build:native
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: darwin-x64
|
||||
- run: pnpm build
|
||||
- name: Intel Mach-O no-clang object contract
|
||||
run: |
|
||||
|
||||
@@ -61,37 +61,48 @@ jobs:
|
||||
runner: macos-15
|
||||
helper: llvm-darwin-arm64
|
||||
runtime: runtime-darwin-arm64
|
||||
cli: cli-darwin-arm64
|
||||
- platform: darwin-x64
|
||||
runner: macos-15-intel
|
||||
helper: llvm-darwin-x64
|
||||
runtime: runtime-darwin-x64
|
||||
cli: cli-darwin-x64
|
||||
- platform: linux-x64
|
||||
runner: ubuntu-24.04
|
||||
helper: llvm-linux-x64-gnu
|
||||
runtime: runtime-linux-x64-gnu
|
||||
cli: cli-linux-x64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
use_zig: true
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
helper: llvm-linux-arm64-gnu
|
||||
runtime: runtime-linux-arm64-gnu
|
||||
cli: cli-linux-arm64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
use_zig: true
|
||||
- platform: windows-x64
|
||||
runner: windows-2022
|
||||
helper: llvm-win32-x64-msvc
|
||||
runtime: runtime-win32-x64-msvc
|
||||
cli: cli-win32-x64-msvc
|
||||
use_zig: true
|
||||
- platform: linux-x64-musl
|
||||
runner: ubuntu-24.04
|
||||
helper: llvm-linux-x64-musl
|
||||
runtime: runtime-linux-x64-musl
|
||||
cli: cli-linux-x64-musl
|
||||
seed_helper: llvm-linux-x64-gnu
|
||||
cli_target: x86_64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
use_zig: true
|
||||
- platform: linux-arm64-musl
|
||||
runner: ubuntu-24.04-arm
|
||||
helper: llvm-linux-arm64-musl
|
||||
runtime: runtime-linux-arm64-musl
|
||||
cli: cli-linux-arm64-musl
|
||||
seed_helper: llvm-linux-arm64-gnu
|
||||
cli_target: aarch64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
use_zig: true
|
||||
- platform: wasm32-wasi
|
||||
@@ -171,12 +182,25 @@ jobs:
|
||||
run: |
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
pnpm --filter @scriptc/${{ matrix.runtime }} build:native
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Build host helper for cross-libc bootstrap
|
||||
if: matrix.seed_helper != ''
|
||||
run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native
|
||||
- name: Build native CLI distribution
|
||||
if: matrix.cli != ''
|
||||
env:
|
||||
SCRIPTC_TARGET: ${{ matrix.cli_target }}
|
||||
run: |
|
||||
pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
pnpm --filter @scriptc/${{ matrix.cli }} build:native
|
||||
node scripts/verify-native-cli.mjs packages/${{ matrix.cli }} --run
|
||||
- name: Upload native packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-${{ matrix.platform }}
|
||||
path: |
|
||||
packages/${{ matrix.helper }}
|
||||
packages/${{ matrix.runtime }}
|
||||
${{ matrix.cli != '' && format('packages/{0}/dist', matrix.cli) || '' }}
|
||||
retention-days: 1
|
||||
|
||||
build-mobile-runtime-packs:
|
||||
@@ -266,12 +290,13 @@ jobs:
|
||||
# upload-artifact does not preserve executable bits. Restore the
|
||||
# helper mode before pnpm runs its prepack checks.
|
||||
find packages -type f -path '*/bin/scriptc-llvm-codegen' -exec chmod 755 {} +
|
||||
find packages -type f \( -path '*/dist/bin/scriptc' -o -path '*/dist/lib/typescript/lib/tsc' -o -path '*/dist/lib/scriptc-comptime' \) -exec chmod 755 {} +
|
||||
pnpm -r build
|
||||
|
||||
- name: Check version sync
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
for pkg in packages/runtime packages/runtime-darwin-arm64 packages/llvm-darwin-arm64 packages/runtime-linux-x64-gnu packages/llvm-linux-x64-gnu packages/runtime-linux-arm64-gnu packages/llvm-linux-arm64-gnu packages/runtime-linux-x64-musl packages/llvm-linux-x64-musl packages/runtime-linux-arm64-musl packages/llvm-linux-arm64-musl packages/runtime-wasm32-wasi packages/runtime-ios-arm64 packages/runtime-ios-simulator-arm64 packages/runtime-android-arm64 packages/runtime-win32-x64-msvc packages/llvm-win32-x64-msvc packages/compiler packages/cli; do
|
||||
for pkg in packages/runtime packages/runtime-* packages/llvm-* packages/compiler packages/cli packages/cli-*; do
|
||||
V=$(node -p "require('./$pkg/package.json').version")
|
||||
if [ "$V" != "$VERSION" ]; then
|
||||
echo "Version mismatch: $pkg is $V, expected $VERSION"
|
||||
@@ -356,6 +381,8 @@ jobs:
|
||||
publish_dir packages/runtime
|
||||
publish_dir packages/runtime-darwin-arm64
|
||||
publish_dir packages/llvm-darwin-arm64 "$HELPER_TARBALL"
|
||||
publish_dir packages/runtime-darwin-x64
|
||||
publish_dir packages/llvm-darwin-x64
|
||||
publish_dir packages/runtime-linux-x64-gnu
|
||||
publish_dir packages/llvm-linux-x64-gnu
|
||||
publish_dir packages/runtime-linux-arm64-gnu
|
||||
@@ -371,19 +398,13 @@ jobs:
|
||||
publish_dir packages/runtime-win32-x64-msvc
|
||||
publish_dir packages/llvm-win32-x64-msvc
|
||||
publish_dir packages/compiler
|
||||
for pkg in packages/cli-*; do publish_dir "$pkg"; done
|
||||
publish_dir packages/cli
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# The GitHub release is a tag, notes, and one asset: the surface
|
||||
# manifest (packages/compiler/surface-manifest.json — the machine-
|
||||
# readable listing of the surface the static tier compiles at this
|
||||
# version, regenerated here and verified against the committed file).
|
||||
# The platform helper ships through its npm package rather than as a GitHub
|
||||
# release asset, so this job runs AFTER a successful npm publish and never
|
||||
# gates it. The body is the CHANGELOG.md block between the
|
||||
# release:start/release:end markers, which RELEASING.md keeps on the
|
||||
# latest entry only.
|
||||
# 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
|
||||
needs: [check-release, publish]
|
||||
@@ -427,6 +448,16 @@ jobs:
|
||||
fi
|
||||
echo "Extracted release notes for $VERSION ($LINES lines)"
|
||||
|
||||
- name: Download native distributions and runtime packs
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: native-*
|
||||
path: packages
|
||||
merge-multiple: true
|
||||
|
||||
- name: Package standalone compilers
|
||||
run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
@@ -445,5 +476,6 @@ jobs:
|
||||
# Attach the surface manifest (idempotent: --clobber makes
|
||||
# re-runs replace the asset instead of failing).
|
||||
gh release upload "$TAG" packages/compiler/surface-manifest.json --clobber
|
||||
gh release upload "$TAG" /tmp/scriptc-release-assets/* --clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -21,6 +21,7 @@ output/
|
||||
/packages/runtime-darwin-arm64/runtime-pack.json
|
||||
/packages/runtime-darwin-arm64/.runtime-pack-*
|
||||
/packages/llvm-*/bin/
|
||||
/packages/cli/bin/
|
||||
/packages/runtime-*/artifacts/
|
||||
/packages/runtime-*/runtime-pack.json
|
||||
/packages/runtime-*/.runtime-pack-*
|
||||
|
||||
@@ -15,7 +15,7 @@ scriptc is experimental and targets macOS, Linux, Windows, and WebAssembly via W
|
||||
|
||||
## Installation
|
||||
|
||||
The compiler requires Node.js 24 or newer. `--emit=ir|llvm` needs only Node. `--emit=asm|obj` uses the matching optional platform helper installed with scriptc on supported macOS, Linux, and Windows hosts (and for WASI), but needs no compiler, archiver, linker, or SDK. Ordinary LLVM executable builds need a platform linker driver and SDK/sysroot, but use the bundled helper plus precompiled runtime pack rather than compiling generated or runtime C. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. The executables it produces do not require Node.
|
||||
The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation, development from a source checkout, the JavaScript compiler API, and `scriptc run` of WASI modules.
|
||||
|
||||
```console
|
||||
$ npm install -g scriptc
|
||||
|
||||
+1
-9
@@ -14,15 +14,7 @@ To prepare a release:
|
||||
|
||||
CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag `v<version>` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`.
|
||||
|
||||
The release job builds and strips each pinned LLVM helper on its matching host,
|
||||
then builds the matching precompiled runtime pack before publishing the
|
||||
constrained platform packages ahead of `@scriptc/compiler`. Ordinary LLVM-tier
|
||||
executables use the helper for the program object and the platform pack for
|
||||
runtime objects; the user's toolchain performs only the final platform link.
|
||||
Explicit C builds, LLVM refusals, and `--sanitize` retain the external C
|
||||
toolchain path. npm postinstall skips local runtime-cache compilation when the
|
||||
platform pack is available. The GitHub release remains a tag, release notes, and the
|
||||
manifest asset; the npm publish never waits on the GitHub release.
|
||||
The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
|
||||
Each published package must have a GitHub Actions trusted publisher for
|
||||
|
||||
@@ -69,9 +69,13 @@ const results = await Promise.all(jobs); // number[] — compiles
|
||||
**The `any`/`unknown` boundary**
|
||||
|
||||
- `any` without `--dynamic` is generally a compile error (SC2011) — use `unknown` and a checked cast, or opt into the engine. Native Map and Set type arguments may use `any`; their slots use the same checked values as `unknown` without embedding an engine.
|
||||
- `any` and `unknown` ride locals, parameters, and returns; record and tuple fields hold `unknown`. JavaScript instance fields can also hold native checked values without a JavaScript engine: bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any`/`unknown` class fields, static checked-value fields, statically typed array elements, and union arms remain unsupported in static builds. Program class instances can pass through `unknown` and recover at their exact static class type without losing identity, including classes with internal collections. Dynamic property inspection of a class whose own fields cannot cross the checked-value boundary throws a catchable `TypeError`. Supported native handles also retain identity; JSON-shaped records and arrays retain the copy boundary described below.
|
||||
- `any` and `unknown` ride locals, parameters, and returns; record and tuple fields hold `unknown`. JavaScript instance fields can also hold native checked values without a JavaScript engine: bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any`/`unknown` class fields, static checked-value fields, statically typed array elements, and union arms remain unsupported in static builds. Program class instances can pass through `unknown` and recover at their exact static class type or a base class without losing identity, including classes with internal collections. Dynamic property inspection of a class whose own fields cannot cross the checked-value boundary throws a catchable `TypeError`. Supported native handles also retain identity. JavaScript argument and return conversions preserve native array references; other JSON-shaped record and array conversions retain the copy boundary described below.
|
||||
- Operations on `unknown` beyond the supported surface (truthiness, `typeof` narrowing, property access, `+`, `switch`, `throw`) need a checked cast first.
|
||||
|
||||
**Three.js**
|
||||
|
||||
With `--npm-static=three`, published three.js modules support CPU math, geometry, scene graphs, materials, and a subset of raycasting in native and WASI builds without a JavaScript engine. Raycasting covers mesh intersections, sorted results, recursive traversal, layers, distance limits, perspective and orthographic camera rays, lines, and points. Mesh attribute interpolation can still fail when three.js reads components beyond the end of a vertex attribute; numeric fields cannot retain the resulting `undefined`. WebGL rendering, browser DOM integration, and native graphics windows remain unsupported.
|
||||
|
||||
**Type surface**
|
||||
|
||||
scriptc typechecks your program in its own type world: the standard `es2025` lib plus its own ambient declarations. In enumerated places those declarations are deliberately different from stock lib or `@types/node`, typed as what actually compiles — `JSON.parse` returns `unknown` (not `any`), and the Promise executor's reject reason is pinned to `Error`. So "programs that typecheck" means scriptc's type world, and a program clean under its own `tsc` can still be redirected here — but never with a bare type error it can't reproduce: the tightened declarations get a second-chance preflight (a program clean in the project's own type world passes, and the affected sites meet per-site diagnostics with rewrite hints instead), and where the shipped fallback declarations lack a lowering for a stock member (`console.table`, `console.time`, ...), the member is declared anyway so the call lands on SC2020 naming the supported alternative. `console.log`/`info`/`debug`/`error`/`warn` themselves take `unknown` arguments and render with Node's console semantics — strings verbatim, everything else through the static `util.inspect`.
|
||||
@@ -92,7 +96,7 @@ A static-tier program otherwise produces byte-identical stdout and the same exit
|
||||
|
||||
**A lying cast on dynamic data throws instead of corrupting memory** — the headline divergence, and the point. `JSON.parse(s) as Config` with mismatched data throws a catchable error naming the offending path (`expected number at $.port, got string`) where JS would silently hand you garbage.
|
||||
|
||||
**Structural width subtyping copies.** A record flowing into a strict field-subset shape is copied, not aliased: mutations through the narrower reference are invisible to the original. Existing typed records and arrays also cross the checked boundary as snapshots. Fresh dense array literals created directly in checked storage preserve their own identity, and supported class instances and native handles cross by reference.
|
||||
**Structural width subtyping copies.** A record flowing into a strict field-subset shape is copied, not aliased: mutations through the narrower reference are invisible to the original. Typed records and arrays generally cross the checked-value boundary as copies. JavaScript argument and return conversions preserve native array references, and class instances retain identity at their exact class type or a base class. Fresh dense array literals created directly in checked storage preserve their own identity, and supported native handles cross by reference.
|
||||
|
||||
**`Object.keys`/`values`/`entries` and `JSON.stringify` report a record's declaration order**, not per-object insertion order. Identical to Node whenever objects are built in declaration order (the overwhelmingly common case).
|
||||
|
||||
@@ -137,10 +141,14 @@ Use [Native FFI](/ffi#replacing-a-node-api-addon) to call the underlying native
|
||||
|
||||
## WASI target limits
|
||||
|
||||
The production <code>wasm32-wasi</code> target supports the complete executable language tier through LLVM: async/await, promises, generators, timers and other portable event-loop work, stdin/readline, filesystem callbacks and promises, and the <code>--dynamic</code> island. Portable WASI Preview 1 has no socket, process-spawn, OS-signal, network-interface, or filesystem-notification capabilities, so networking/fetch, child processes, signal APIs, <code>os.networkInterfaces()</code>, and <code>fs.watch</code> are rejected before linking with <code>SC3002</code>. <code>--sanitize</code>, native FFI, and library-mode archive builds are also unavailable. Filesystem access is bounded by the host's preopens; <code>scriptc run</code> exposes the current working directory and <code>/tmp</code>. See [Platform Support](/platforms) for build and run details.
|
||||
The production <code>wasm32-wasi</code> target supports the complete executable language tier through LLVM: async/await, promises, generators, timers and other portable event-loop work, stdin/readline, filesystem callbacks and promises, and the <code>--dynamic</code> island. Portable WASI Preview 1 has no socket, process-spawn, OS-signal, network-interface, or filesystem-notification capabilities, so networking/fetch, child processes, signal APIs, <code>os.networkInterfaces()</code>, and <code>fs.watch</code> are rejected before linking with <code>SC3002</code>. <code>--sanitize</code> and native FFI are also unavailable. Library mode emits an async-free Wasm reactor with named host imports; native runtime localization and thread instances are refused. Each Wasm instance owns independent state. Filesystem access is bounded by the host's preopens; <code>scriptc run</code> exposes the current working directory and <code>/tmp</code>. See [Platform Support](/platforms) for build and run details.
|
||||
|
||||
## Tooling gaps
|
||||
|
||||
- `scriptc run` does not forward extra CLI arguments to the program — `build` and invoke the binary directly.
|
||||
- Native FFI is a direct, manifest-declared C ABI link surface. Formats 2–5 cover call-scoped callbacks, copied string/byte callback parameters, explicitly released retained callbacks, and asynchronous foreign-thread delivery. Format 6 adds single-precision floats, signed 8-bit and signed/unsigned 16-bit integers, and borrowed writable byte spans. Variadic calls, structs by value, owned pointer/string/byte returns, runtime dynamic-library loading, and library-mode builds remain unsupported. See [Native FFI](/ffi).
|
||||
- Numbers are JS-exact f64 everywhere. Integer inference and ownership analysis — the systems-language performance ceiling — are roadmap, not shipped.
|
||||
|
||||
## Browser embedding
|
||||
|
||||
Wasm reactors need a host-provided WASI Preview 1 adapter. DOM APIs, browser graphics bindings, and three.js <code>WebGLRenderer</code> are not compiled today. Supported three.js CPU operations can execute inside a reactor while an external web application renders their results. Library mode excludes promises, async functions, timers, and the JavaScript island. See [WebAssembly Modules](/wasm) for the embedding contract.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Owned LLVM targets
|
||||
|
||||
Source artifacts selected with <code>--emit=ir|llvm</code> need only Node. On supported hosts, <code>--emit=asm|obj</code> uses the version-matched platform helper installed with scriptc and does not invoke a compiler, archiver, linker, or SDK. Supported assembly/object targets are macOS arm64/x64 (macOS 14.0 artifacts; helper needs macOS 15+), Linux x64/arm64 glibc, Windows x64 MSVC, Linux x64/arm64 musl, and WASI Preview 1. Ordinary LLVM executables additionally need the platform linker and SDK/sysroot; the helper emits the program object and the packaged runtime supplies objects/archives, so that driver compiles neither program nor runtime C. Runtime development with <code>--sanitize</code> requires an external LLVM toolchain and a C compiler for instrumentation. Object output retains undefined runtime references and is not a library archive.
|
||||
The compiler runs natively and bundles its TypeScript checker and LLVM helper. Outputs selected with <code>--emit=ir|llvm|asm|obj</code> need no Node installation or external compiler, archiver, linker, or SDK. Supported assembly/object targets are macOS arm64/x64 (macOS 14.0 artifacts; helper needs macOS 15+), Linux x64/arm64 glibc, Windows x64 MSVC, Linux x64/arm64 musl, and WASI Preview 1. Executables additionally need the platform linker and SDK/sysroot; the helper emits the program object and the packaged runtime supplies objects/archives. Runtime development with <code>--sanitize</code> requires an external LLVM toolchain and a C compiler for instrumentation. Object output retains undefined runtime references and is not a library archive.
|
||||
|
||||
## Cross-compilation via zig
|
||||
|
||||
@@ -57,16 +57,16 @@ The full library-mode feature set applies: profile-declared exports and ABI entr
|
||||
|
||||
### WebAssembly (WASI Preview 1)
|
||||
|
||||
Set <code>SCRIPTC_TARGET=wasm32-wasi</code> to produce a standalone <code>.wasm</code> module. Without <code>-o</code>, <code>scriptc build hello.ts</code> writes <code>.scriptc/hello.wasm</code>. <code>scriptc run</code> hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as <code>/</code>, and maps the host platform's temporary directory to the guest's <code>/tmp</code>. A built module can run in another WASI Preview 1 host instead.
|
||||
Set <code>SCRIPTC_TARGET=wasm32-wasi</code> to produce a standalone <code>.wasm</code> module. Without <code>-o</code>, <code>scriptc build hello.ts</code> writes <code>.scriptc/hello.wasm</code>. Compilation does not require Node. <code>scriptc run</code> requires Node.js 24 or newer on <code>PATH</code> to host the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as <code>/</code>, and maps the host platform's temporary directory to the guest's <code>/tmp</code>. A built module can run in another WASI Preview 1 host instead.
|
||||
|
||||
WASI is a production LLVM target with the same language tiers as the native targets. Its 32-bit LLVM ABI supports collections, closures, exceptions, classes, checked dynamic values, async/await, promises, synchronous and asynchronous generators, timers, stdin/readline events, process-exit listeners, filesystem callbacks and promises, and the <code>--dynamic</code> QuickJS island.
|
||||
|
||||
The remaining executable boundary is host capability, not language coverage. WASI Preview 1 has no portable socket, process-spawn, OS-signal, network-interface, or filesystem-notification APIs. Networking/fetch, child processes, signal APIs, <code>os.networkInterfaces()</code>, and <code>fs.watch</code> therefore fail before linking with diagnostic <code>SC3002</code>. <code>--sanitize</code>, native FFI, and library-mode archive builds are unavailable too. Filesystem behavior is bounded by the host's preopens, and process/OS introspection follows WASI's reduced model.
|
||||
The remaining executable boundary is host capability, not language coverage. WASI Preview 1 has no portable socket, process-spawn, OS-signal, network-interface, or filesystem-notification APIs. Networking/fetch, child processes, signal APIs, <code>os.networkInterfaces()</code>, and <code>fs.watch</code> therefore fail before linking with diagnostic <code>SC3002</code>. <code>--sanitize</code> and native FFI are unavailable too. Filesystem behavior is bounded by the host's preopens, and process/OS introspection follows WASI's reduced model.
|
||||
|
||||
## Cross-target limits
|
||||
|
||||
- `--sanitize` is a host-build lane.
|
||||
- Library-mode static archives are native embedder artifacts; <code>wasm32-wasi</code> rejects <code>scriptc build --lib</code> with <code>SC3002</code>.
|
||||
- On <code>wasm32-wasi</code>, <code>scriptc build --lib --profile</code> emits a callable Wasm reactor instead of a static archive. See [WebAssembly Modules](/wasm) for exports, host imports, initialization, and memory ownership.
|
||||
- Mobile triples are library-mode-only: standalone executable builds reject with <code>SC3002</code>, and only the library-admissible surface (the async-free static tier a library archive links) is supported there.
|
||||
- iOS targets build on macOS hosts only (Mach-O localization uses the host linker); Android archives build from any supported host with Zig.
|
||||
- WASI cannot host APIs for capabilities missing from Preview 1, as described above.
|
||||
|
||||
@@ -4,9 +4,9 @@ Install the CLI from npm and compile your first binary in a couple of minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **macOS arm64** is the primary platform ([Linux and Windows](/platforms) are cross-compilation targets).
|
||||
- **Node ≥ 24** — to run the compiler. The binaries it produces need no Node at all.
|
||||
- **clang** — preinstalled with the Xcode Command Line Tools; required for executable builds, but not for <code>--emit=ir|c|llvm|asm|obj</code>. Assembly/object output uses the matching helper installed with scriptc and requires macOS 15+.
|
||||
- **macOS 15+ arm64/x64, Linux arm64/x64, or Windows x64** — see [platform support](/platforms) for target details.
|
||||
- **Node ≥ 24 and npm** — for npm installation. The installed compiler and its native output run without Node. Standalone distributions are also available from [GitHub Releases](https://github.com/vercel-labs/scriptc/releases).
|
||||
- **A platform linker and SDK** — for executable builds. On macOS, install the Xcode Command Line Tools. <code>--emit=ir|llvm|asm|obj</code> uses bundled tools and needs no external linker or SDK.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -14,6 +14,8 @@ Install the CLI from npm and compile your first binary in a couple of minutes.
|
||||
$ npm install -g scriptc
|
||||
```
|
||||
|
||||
Keep optional dependencies and installation scripts enabled so npm can install the native command for your platform.
|
||||
|
||||
To work from a clone instead (`pnpm install && pnpm build` in the repo, then `pnpm scriptc` from the repo directory), see the [repository](https://github.com/vercel-labs/scriptc).
|
||||
|
||||
## Your first binary
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("wasm");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# WebAssembly Modules
|
||||
|
||||
Compile a reusable module with <code>SCRIPTC_TARGET=wasm32-wasi</code> and <code>scriptc build --lib --profile</code>. The output is a WASI Preview 1 reactor: a <code>.wasm</code> file with callable exports, named host imports, and its own memory. Application code runs as compiled Wasm, without an embedded JavaScript engine.
|
||||
|
||||
Your web application owns loading, browser events, animation scheduling, and rendering. It supplies the module's imports and calls its exports. scriptc does not generate a web application or a graphics adapter.
|
||||
|
||||
## Define the interface
|
||||
|
||||
The library profile names the entry module and its public functions. A reachable callback call becomes a synchronous import from the <code>scriptc</code> namespace.
|
||||
|
||||
```ts:counter.ts
|
||||
declare function changed(value: number): void;
|
||||
let count = 0;
|
||||
export function step(delta: number): number {
|
||||
count += delta;
|
||||
changed(count);
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
```json:counter.json
|
||||
{
|
||||
"profile_format": 1,
|
||||
"name": "counter",
|
||||
"entry": "counter.ts",
|
||||
"emission": "llvm",
|
||||
"abi": {
|
||||
"prefix": "counter_",
|
||||
"init_symbol": "counter_init",
|
||||
"sink_register_symbol": "counter_sink",
|
||||
"callback_register_symbol": "counter_callback"
|
||||
},
|
||||
"callbacks": [{ "name": "changed", "params": ["f64"], "returns": "void" }],
|
||||
"exports": [{ "export": "step", "symbol": "counter_step", "params": ["f64"], "returns": "f64" }]
|
||||
}
|
||||
```
|
||||
|
||||
Build with the <code>wasm32-wasi</code> target and select the output with <code>-o counter.wasm</code>. Without <code>-o</code>, the output is <code>.scriptc/counter.wasm</code> beside the profile. The compiler API's <code>compileLibrary()</code> returns this path in <code>archivePath</code>, matching the existing native library API.
|
||||
|
||||
The profile uses the native library schema. <code>sink_register_symbol</code> and <code>callback_register_symbol</code> name native registration functions; a Wasm reactor does not export them. Wasm binds callbacks during instantiation and delivers panics through <code>scriptc.panic</code>.
|
||||
|
||||
Set <code>"optimization": "dev"</code> in the profile for faster builds. Eligible npm dependencies are attempted automatically. A profile may add <code>"npm_static": ["three"]</code> to explicitly attempt compilation from a package's source, including packages without bundled declarations. Unsupported reachable code still produces a compiler diagnostic.
|
||||
|
||||
## Instantiate and initialize
|
||||
|
||||
A browser host needs a WASI Preview 1 adapter as well as the application's callbacks. Browsers do not supply WASI automatically. Inspect <code>WebAssembly.Module.imports(module)</code> to see the exact imports required by your compiled program. Supply real implementations or explicit failures for unsupported operations.
|
||||
|
||||
```js:loader.js
|
||||
export async function loadCounter(url, wasi) {
|
||||
const module = await WebAssembly.compileStreaming(fetch(url));
|
||||
let api;
|
||||
const instance = await WebAssembly.instantiate(module, {
|
||||
...wasi.getImportObject(),
|
||||
scriptc: {
|
||||
changed(value) {
|
||||
console.log(value);
|
||||
},
|
||||
panic(pointer, length) {
|
||||
const bytes = new Uint8Array(api.memory.buffer, pointer, length);
|
||||
throw new Error(new TextDecoder().decode(bytes));
|
||||
},
|
||||
},
|
||||
});
|
||||
api = instance.exports;
|
||||
wasi.initialize(instance);
|
||||
api.counter_init();
|
||||
return api;
|
||||
}
|
||||
```
|
||||
|
||||
This loader assumes an adapter with <code>getImportObject()</code> and <code>initialize(instance)</code> methods, as provided by Node's <code>node:wasi</code>. Browser adapters may use different method names. Initialization must first connect the adapter to the instance's exported memory and invoke <code>_initialize</code> once, then call the profile's <code>init_symbol</code>. Do not call a command-style <code>start()</code>; reactors have no <code>_start</code> export.
|
||||
|
||||
After initialization, <code>counter_step(2)</code> returns <code>2</code>, and another call returns <code>4</code>. Calling <code>counter_init()</code> again resets application globals and reevaluates module initialization. Instantiate the module again for independent state and memory.
|
||||
|
||||
## Values and memory ownership
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Profile class</th><th>Wasm boundary</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>f64</code></td><td>JavaScript number.</td></tr>
|
||||
<tr><td><code>bool</code>, <code>u8</code>, <code>u32</code>, <code>i32</code></td><td>Wasm i32; booleans use 0 or 1. u32 values may appear as signed JavaScript numbers; use <code>value >>> 0</code> to read them unsigned.</td></tr>
|
||||
<tr><td><code>i64</code>, <code>u64</code></td><td>JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.</td></tr>
|
||||
<tr><td><code>string</code>, <code>bytes</code> parameters</td><td>Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.</td></tr>
|
||||
<tr><td><code>string</code>, <code>bytes</code> results</td><td>Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.</td></tr>
|
||||
<tr><td><code>void</code></td><td>No result.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The native profile's direction restrictions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host.
|
||||
|
||||
Allocate host input buffers and output slots with <code>scriptc_alloc(size)</code>; a zero result indicates allocation failure. Release each allocation exactly once with <code>scriptc_free(pointer)</code>. Only free pointers returned by <code>scriptc_alloc</code>. These allocations remain live across application calls, initialization, and collection until freed.
|
||||
|
||||
Result buffers belong to the module. If the profile declares <code>abi.result_reset_symbol</code>, results accumulate until that function is called. Otherwise, the next application export resets the result arena. Initialization and the optional <code>abi.collect_symbol</code> also invalidate results. Allocation and free do not reset the arena. Copy a result into host-owned storage before invalidating it; never pass its pointer to <code>scriptc_free</code>.
|
||||
|
||||
Recreate typed arrays and DataViews from <code>memory.buffer</code> after every Wasm call that may allocate. Memory growth replaces the backing buffer and invalidates older views. Pointers remain offsets into the instance's memory; never use a pointer with another instance.
|
||||
|
||||
## Errors and scheduling
|
||||
|
||||
A compiled trap or escaped exception calls <code>scriptc.panic(pointer, length)</code> with the native library's structured diagnostic bytes, then traps if the callback returns. Discard the instance after a panic, a Wasm trap, or an exception escaping a host import. Recovery uses a new instance; calling the initialization export does not repair a failed instance.
|
||||
|
||||
Host callbacks run synchronously. They must return before calling any application export, initialization, collection, allocation, or free on the same instance. Reentry is rejected. Copy callback data and schedule subsequent work after the outer call returns. The web app can call a frame export from <code>requestAnimationFrame</code> and render its results through Canvas, WebGL, or WebGPU.
|
||||
|
||||
See [Limitations](/limitations) for browser graphics and library-mode boundaries.
|
||||
@@ -23,6 +23,7 @@ export const navSections: NavSection[] = [
|
||||
{ name: "npm Dependencies", href: "/dependencies" },
|
||||
{ name: "Native FFI", href: "/ffi" },
|
||||
{ name: "Native Program Objects", href: "/native-objects" },
|
||||
{ name: "WebAssembly Modules", href: "/wasm" },
|
||||
{ name: "Platform Support", href: "/platforms" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
||||
dependencies: "npm Dependencies",
|
||||
ffi: "Native FFI",
|
||||
"native-objects": "Native Program Objects",
|
||||
wasm: "WebAssembly Modules",
|
||||
platforms: "Platform Support",
|
||||
compatibility: "Node.js 24 Compatibility",
|
||||
"how-it-works": "How It Works",
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm -r --filter \"./packages/*\" run build",
|
||||
"build:fresh": "rm -rf packages/compiler/dist packages/cli/dist node_modules/.cache/scriptc-tsc && pnpm build",
|
||||
"build:native-compiler": "node --max-old-space-size=8192 --import tsx scripts/build-native-compiler.mts",
|
||||
"build:native-compiler": "node --max-old-space-size=8192 --import tsx scripts/build-native-cli.mts",
|
||||
"bench:builds": "node scripts/bench-builds.mjs",
|
||||
"test": "vitest run",
|
||||
"test:ts7": "node scripts/test-ts7.mjs",
|
||||
@@ -31,6 +31,7 @@
|
||||
"effect": "3.22.2",
|
||||
"eslint": "^9.20.0",
|
||||
"js-yaml": "4.3.0",
|
||||
"rollup": "4.62.2",
|
||||
"three": "0.186.1",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-darwin-arm64"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-darwin-x64"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-linux-arm64-gnu"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-linux-arm64-musl"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-linux-x64-gnu"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-linux-x64-musl"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.1.7",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/scriptc.git",
|
||||
"directory": "packages/cli-win32-x64-msvc"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ $ scriptc build fib.ts -o fib && ./fib
|
||||
$ npm install -g scriptc
|
||||
```
|
||||
|
||||
Requires Node.js 24. Executable builds require a platform linker driver and SDK/sysroot. On supported macOS, Linux, and Windows hosts, LLVM executables use the matching optional helper and precompiled runtime pack, so the driver only links; select that driver with `SCRIPTC_LINKER`. Runtime development with `--sanitize` additionally needs a C compiler. `--emit=ir|llvm` requires only Node, while `--emit=asm|obj` requires neither an external compiler nor a linker.
|
||||
The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation and `scriptc run` of WASI modules.
|
||||
|
||||
Builds use a bounded persistent cache. Unchanged source can reuse the validated frontend result and LLVM program objects. Library identity getters occupy a separate LLVM module, so an identity change can reuse the large program object. Runtime objects come from the installed pack. Executable cache entries verify their native dependencies; FFI builds relink against current external inputs. Set `SCRIPTC_NO_CACHE=1` to bypass the cache or `SCRIPTC_CACHE_DIR` to select its location. An existing POSIX override must already be private.
|
||||
|
||||
@@ -38,7 +38,7 @@ For embedder-hosted modules that are not installed npm packages, coverage can ma
|
||||
|
||||
No annotations, no dialect, no special stdlib: the same TypeScript you run on Node, type-checked by the real TypeScript compiler. Programs outside the static tier can opt into `--dynamic`, which embeds a small JavaScript engine (~620KB) for the parts that can't be static; everything else fails the build with a specific error code and usually a rewrite hint.
|
||||
|
||||
WebAssembly is available as a production LLVM target: `SCRIPTC_TARGET=wasm32-wasi scriptc build app.ts`. It emits a WASI Preview 1 `.wasm` module, and `scriptc run` supplies a WASI host. The complete executable language tier—including async, generators, timers, and `--dynamic`—is supported. APIs needing capabilities WASI P1 does not expose (network sockets/fetch, child processes, OS signals, and filesystem watching), sanitizer builds, native FFI, and library-mode archive builds are rejected with `SC3002`.
|
||||
WebAssembly is available as a production LLVM target: `SCRIPTC_TARGET=wasm32-wasi scriptc build app.ts`. It emits a WASI Preview 1 `.wasm` module, and `scriptc run` supplies a WASI host. The complete executable language tier—including async, generators, timers, and `--dynamic`—is supported. APIs needing capabilities WASI P1 does not expose (network sockets/fetch, child processes, OS signals, and filesystem watching), sanitizer builds, and native FFI are rejected with `SC3002`. With `--lib --profile`, the target produces a callable Wasm reactor with named host imports and per-instance memory. See [WebAssembly Modules](https://scriptc.dev/wasm) for the embedding contract.
|
||||
|
||||
Native code can be called through an explicit, link-time C ABI manifest: declare the function signature in TypeScript, bind it to a C symbol, and build with `--ffi <manifest.json>`. See the [Native FFI guide](https://scriptc.dev/ffi).
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.1.7",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables — no Node, no V8, no JavaScript engine in the binary",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables \u2014 no Node, no V8, no JavaScript engine in the binary",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
@@ -11,19 +11,41 @@
|
||||
},
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"scriptc": "dist/bootstrap.js"
|
||||
"scriptc": "bin/scriptc.exe"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"scripts"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|
||||
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json",
|
||||
"prepack": "node scripts/prepare-native.mjs",
|
||||
"postinstall": "node scripts/install-native.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@scriptc/compiler": "workspace:*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@scriptc/cli-darwin-arm64": "workspace:*",
|
||||
"@scriptc/cli-darwin-x64": "workspace:*",
|
||||
"@scriptc/cli-linux-arm64-gnu": "workspace:*",
|
||||
"@scriptc/cli-linux-arm64-musl": "workspace:*",
|
||||
"@scriptc/cli-linux-x64-gnu": "workspace:*",
|
||||
"@scriptc/cli-linux-x64-musl": "workspace:*",
|
||||
"@scriptc/cli-win32-x64-msvc": "workspace:*",
|
||||
"@scriptc/runtime-darwin-arm64": "workspace:*",
|
||||
"@scriptc/runtime-darwin-x64": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-musl": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-musl": "workspace:*",
|
||||
"@scriptc/runtime-win32-x64-msvc": "workspace:*",
|
||||
"@scriptc/runtime-wasm32-wasi": "workspace:*",
|
||||
"@scriptc/runtime-ios-arm64": "workspace:*",
|
||||
"@scriptc/runtime-ios-simulator-arm64": "workspace:*",
|
||||
"@scriptc/runtime-android-arm64": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { constants, copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function nativeCliPackage(platform, architecture, libc) {
|
||||
const host = `${platform}-${architecture}`;
|
||||
if (host === "darwin-arm64" || host === "darwin-x64") return `@scriptc/cli-${host}`;
|
||||
if (host === "win32-x64") return "@scriptc/cli-win32-x64-msvc";
|
||||
if (host === "linux-x64" || host === "linux-arm64") {
|
||||
if (libc !== "glibc" && libc !== "musl") throw new Error("could not identify the Linux C library");
|
||||
return `@scriptc/cli-${host}-${libc === "musl" ? "musl" : "gnu"}`;
|
||||
}
|
||||
throw new Error(`scriptc has no native command for ${host}`);
|
||||
}
|
||||
|
||||
function hostLibc() {
|
||||
if (process.platform !== "linux") return null;
|
||||
const report = process.report.getReport();
|
||||
if (report.header.glibcVersionRuntime) return "glibc";
|
||||
if (report.sharedObjects.some((path) => /(?:^|\/)ld-musl-|libc\.musl-/.test(path))) return "musl";
|
||||
// A statically linked Node has no loaded libc in its process report.
|
||||
// Only select musl when its loader is present for this architecture.
|
||||
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
||||
return existsSync(`/lib/ld-musl-${arch}.so.1`) ? "musl" : null;
|
||||
}
|
||||
|
||||
export function relocateToolchain(manifest, sourceDirectory, destinationDirectory) {
|
||||
const relocated = { ...manifest };
|
||||
const pathFrom = (value) => relative(destinationDirectory, resolve(sourceDirectory, value));
|
||||
for (const name of ["ts7", "llvm_package", "runtime_pack", "runtime_sources", "declarations", "comptime", "wasi_node_runner"]) {
|
||||
if (typeof relocated[name] === "string") relocated[name] = pathFrom(relocated[name]);
|
||||
}
|
||||
for (const name of ["linker", "dsymutil", "archiver", "relocatable_linker"]) {
|
||||
const value = relocated[name];
|
||||
if (typeof value === "string" && !isAbsolute(value) && /[/\\]/.test(value)) relocated[name] = pathFrom(value);
|
||||
}
|
||||
if (relocated.runtime_packs) relocated.runtime_packs = relocated.runtime_packs.map((pack) => ({ ...pack, path: pathFrom(pack.path) }));
|
||||
return relocated;
|
||||
}
|
||||
|
||||
export function installNativeCli(directory, packageName = nativeCliPackage(process.platform, process.arch, hostLibc())) {
|
||||
directory = realpathSync(directory);
|
||||
const require = createRequire(join(directory, "package.json"));
|
||||
const packageManifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||
const version = packageManifest.version;
|
||||
let platformManifest;
|
||||
try { platformManifest = require.resolve(`${packageName}/package.json`); }
|
||||
catch { throw new Error(`scriptc requires ${packageName}@${version}; reinstall with optional dependencies enabled`); }
|
||||
const identity = JSON.parse(readFileSync(platformManifest, "utf8"));
|
||||
if (identity.name !== packageName || identity.version !== version) throw new Error(`scriptc requires ${packageName}@${version}, found ${identity.version}`);
|
||||
const sourceDirectory = join(dirname(platformManifest), "dist", "bin");
|
||||
const source = join(sourceDirectory, process.platform === "win32" ? "scriptc.exe" : "scriptc");
|
||||
const original = JSON.parse(readFileSync(source + ".json", "utf8"));
|
||||
if (original.schema !== "scriptc.native-toolchain.v1" || original.compiler_version !== version) throw new Error("native compiler toolchain version does not match this installation");
|
||||
const bin = join(directory, "bin");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
const manifest = relocateToolchain(original, sourceDirectory, bin);
|
||||
const packs = new Map((manifest.runtime_packs ?? []).map((pack) => [pack.target, pack]));
|
||||
for (const suffix of ["darwin-arm64", "darwin-x64", "linux-x64-gnu", "linux-arm64-gnu", "linux-x64-musl", "linux-arm64-musl",
|
||||
"win32-x64-msvc", "wasm32-wasi", "ios-arm64", "ios-simulator-arm64", "android-arm64"]) {
|
||||
const name = `@scriptc/runtime-${suffix}`;
|
||||
if (packageManifest.optionalDependencies?.[name] === undefined) continue;
|
||||
let path;
|
||||
try { path = require.resolve(`${name}/package.json`); }
|
||||
catch { continue; }
|
||||
const packIdentity = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (packIdentity.name !== name || packIdentity.version !== version) throw new Error(`scriptc requires ${name}@${version}`);
|
||||
const pack = JSON.parse(readFileSync(join(dirname(path), "runtime-pack.json"), "utf8"));
|
||||
if (pack.schema !== "scriptc.runtime-pack.v1" || pack.package !== name || pack.version !== version) {
|
||||
throw new Error(`invalid runtime pack installed for ${name}@${version}`);
|
||||
}
|
||||
if (!packs.has(pack.target.name)) packs.set(pack.target.name, { target: pack.target.name, path: relative(bin, dirname(path)) });
|
||||
}
|
||||
manifest.runtime_packs = [...packs.values()];
|
||||
// npm normalizes modes for payloads outside package bin entries. These
|
||||
// tools are launched directly by the compiler after installation.
|
||||
if (process.platform !== "win32") {
|
||||
for (const path of [manifest.ts7, manifest.comptime, join(manifest.llvm_package, "bin/scriptc-llvm-codegen")]) {
|
||||
chmodSync(resolve(bin, path), 0o755);
|
||||
}
|
||||
}
|
||||
// The common filename lets npm's Windows shim invoke a native executable.
|
||||
// POSIX bin links also execute this file directly, without a JS launcher.
|
||||
const destination = join(bin, "scriptc.exe");
|
||||
const staged = destination + `.install-${process.pid}`;
|
||||
try {
|
||||
copyFileSync(source, staged, constants.COPYFILE_FICLONE);
|
||||
chmodSync(staged, 0o755);
|
||||
writeFileSync(staged + ".json", JSON.stringify(manifest, null, 2) + "\n");
|
||||
renameSync(staged + ".json", destination + ".json");
|
||||
renameSync(staged, destination);
|
||||
} finally {
|
||||
rmSync(staged, { force: true });
|
||||
rmSync(staged + ".json", { force: true });
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
const directory = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||
// A source checkout builds the native distribution separately. pnpm pack
|
||||
// replaces workspace ranges with release versions before publication.
|
||||
const workspace = Object.values(manifest.optionalDependencies ?? {}).some((value) => String(value).startsWith("workspace:"));
|
||||
if (!workspace) installNativeCli(directory);
|
||||
}
|
||||
catch (error) {
|
||||
process.stderr.write(`scriptc: ${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function prepareNativeCommand(directory) {
|
||||
const bin = join(directory, "bin");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
// npm creates bin links before postinstall. The payload must exist in the
|
||||
// tarball, and no shebang may pin the Windows shim to an interpreter.
|
||||
// Installation replaces this placeholder with the platform executable.
|
||||
writeFileSync(join(bin, "scriptc.exe"),
|
||||
"echo 'scriptc: native installation is incomplete; enable install scripts and reinstall scriptc' >&2\nexit 1\n", { mode: 0o755 });
|
||||
rmSync(join(bin, "scriptc.exe.json"), { force: true });
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
prepareNativeCommand(resolve(dirname(fileURLToPath(import.meta.url)), ".."));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@scriptc/compiler/cli/command";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@scriptc/compiler/cli/host";
|
||||
+20
-337
@@ -1,12 +1,9 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { analyze, compile, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, sourceTargetPlatform, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler";
|
||||
import { resolveOutputOptions } from "./output-options.js";
|
||||
import { selectOutputPaths } from "./paths.js";
|
||||
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
||||
import { analyze, compile, compileLibrary, resolveProvenanceSources, sourceTargetPlatform, warmNativeCaches } from "@scriptc/compiler";
|
||||
import { runCli } from "./command.js";
|
||||
|
||||
/** The version of the installed package. Read from the manifest rather than
|
||||
* baked in by the build, so a stamped release and a source checkout answer
|
||||
@@ -19,334 +16,20 @@ function version(): string {
|
||||
return manifest.version ?? "unknown";
|
||||
}
|
||||
|
||||
/* The exit discipline: NEVER process.exit() after writing output. stdout/
|
||||
* stderr to a PIPE are async streams — process.exit() drops whatever libuv
|
||||
* hasn't flushed yet, which truncates large diagnostic renders at the pipe
|
||||
* buffer (observed: 64KB cut mid-code-frame). Every path sets
|
||||
* process.exitCode and returns instead; Node exits naturally once the
|
||||
* streams drain. */
|
||||
class CliExit extends Error {
|
||||
constructor(readonly code: number) {
|
||||
super(`exit ${code}`);
|
||||
}
|
||||
}
|
||||
|
||||
function fail(msg: string): never {
|
||||
process.stderr.write(msg + "\n");
|
||||
throw new CliExit(1);
|
||||
}
|
||||
|
||||
/** parseArgs, with its throw turned into the CLI's own one-line error.
|
||||
* Unparseable arguments are a USER error — an unknown flag or a missing
|
||||
* value used to reach the top level as an uncaught ERR_PARSE_ARGS_* and
|
||||
* print a Node stack trace over the user's terminal. */
|
||||
function parseCli(): ReturnType<typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>> {
|
||||
try {
|
||||
return parseArgs({ options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
|
||||
} catch (err) {
|
||||
// parseArgs appends a paragraph about `--` and positionals to the
|
||||
// unknown-option message; the first sentence is the part that names
|
||||
// what was wrong, and USAGE below already covers what was meant.
|
||||
const raw = err instanceof Error ? err.message : String(err);
|
||||
const msg = raw.split("\n")[0]!.split(". ")[0]!;
|
||||
fail(`scriptc: ${msg}\n\n${USAGE}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const { values, positionals } = parseCli();
|
||||
const externalTypeArgs = values["external-types"] ?? [];
|
||||
|
||||
if (values.version) {
|
||||
process.stdout.write(`${version()}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
process.stdout.write(USAGE);
|
||||
return values.help ? 0 : 1;
|
||||
}
|
||||
|
||||
const [command, inputArg] = positionals;
|
||||
if (command === "cache") {
|
||||
if (inputArg !== "warm") fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`);
|
||||
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.ffi !== undefined || values.profile !== undefined || values.strip || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-llvm"]) {
|
||||
fail(`scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`);
|
||||
}
|
||||
const optimization = values.optimization;
|
||||
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") {
|
||||
fail(`unknown optimization "${optimization}" (supported: release, dev)\n\n${USAGE}`);
|
||||
}
|
||||
const profileArgs = positionals.slice(2);
|
||||
const knownProfiles = new Set<NativeCacheWarmProfile>(["runtime", "tls", "dynamic"]);
|
||||
for (const profile of profileArgs) {
|
||||
if (!knownProfiles.has(profile as NativeCacheWarmProfile)) {
|
||||
fail(`unknown cache warm profile "${profile}" (supported: runtime, tls, dynamic)`);
|
||||
}
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await warmNativeCaches({
|
||||
...(optimization === undefined ? {} : { optimization }),
|
||||
sanitize: values.sanitize,
|
||||
...(profileArgs.length === 0
|
||||
? {}
|
||||
: { profiles: profileArgs as NativeCacheWarmProfile[] }),
|
||||
});
|
||||
} catch (error) {
|
||||
fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
process.stdout.write(`${result.cacheRoot}\n`);
|
||||
for (const profile of result.profiles) {
|
||||
process.stdout.write(`${profile.profile}\t${Math.round(profile.elapsedMs)}ms\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (command !== "build" && command !== "run" && command !== "coverage") {
|
||||
fail(`unknown command "${command}"\n\n${USAGE}`);
|
||||
}
|
||||
if (values.lib) {
|
||||
// LIBRARY mode: the profile names the entry module and pins the
|
||||
// emission; the executable lane's mode flags have no meaning here
|
||||
// (library artifacts are static-tier only, and there is no fallback
|
||||
// concept — bare npm specifiers are static-or-refuse: the npm-static
|
||||
// eligibility bar runs automatically, eligible packages compile into
|
||||
// the graph, ineligible ones refuse with SC4013).
|
||||
if (command !== "build") fail(`--lib is a build mode (scriptc build --lib --profile <p.json>)\n\n${USAGE}`);
|
||||
const profileArg = values.profile;
|
||||
if (!profileArg) fail(`scriptc build --lib needs --profile <profile.json>\n\n${USAGE}`);
|
||||
if (inputArg) {
|
||||
fail("scriptc build --lib takes no input positional: the profile names the entry module");
|
||||
}
|
||||
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.strip || values.ffi !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--strip/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
|
||||
);
|
||||
}
|
||||
const profilePath = resolve(profileArg);
|
||||
const libOutDir = values.out ? dirname(resolve(values.out)) : join(dirname(profilePath), ".scriptc");
|
||||
const result = await compileLibrary({
|
||||
profilePath,
|
||||
outDir: libOutDir,
|
||||
...(values.out ? { outPath: resolve(values.out) } : {}),
|
||||
emitIr: values["emit-ir"],
|
||||
sanitize: values.sanitize,
|
||||
process.exitCode = await runCli(process.argv.slice(2), {
|
||||
version, analyze: async (entry, options) => analyze(entry, options), compile, compileLibrary, resolveProvenanceSources, sourceTargetPlatform, warmNativeCaches,
|
||||
run: (binary) => new Promise<number>((resolveExit) => {
|
||||
let child;
|
||||
if (sourceTargetPlatform() === "wasi") {
|
||||
const builtRunner = fileURLToPath(new URL("./wasi-runner.js", import.meta.url));
|
||||
const runner = existsSync(builtRunner) ? builtRunner : fileURLToPath(new URL("./wasi-runner.ts", import.meta.url));
|
||||
child = spawn(process.execPath, [...process.execArgv, "--no-warnings", runner, binary], { stdio: "inherit" });
|
||||
} else child = spawn(binary, [], { stdio: "inherit" });
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.stderr.write(`scriptc: program killed by ${signal}\n`);
|
||||
resolveExit(1);
|
||||
} else resolveExit(code ?? 0);
|
||||
});
|
||||
if (!result.ok) {
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n");
|
||||
const n = result.diagnostics.length;
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
return 1;
|
||||
}
|
||||
if (!values["keep-llvm"]) rmSync(result.llvmPath, { force: true });
|
||||
process.stdout.write(`${result.archivePath}\n`);
|
||||
// The contract sidecar rides the same invocation when the profile
|
||||
// declares one — name it so the embedder's tooling knows where to look.
|
||||
if (result.sidecarPath !== undefined) process.stdout.write(`${result.sidecarPath}\n`);
|
||||
return 0;
|
||||
}
|
||||
if (values["emit-ir"] && (command === "build" || command === "run")) {
|
||||
process.stderr.write("scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n");
|
||||
}
|
||||
if (!inputArg) fail(`missing input file\n\n${USAGE}`);
|
||||
const input = resolve(inputArg);
|
||||
if (command === "coverage" && values.emit !== undefined) {
|
||||
fail(`--emit is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (command === "coverage" && values.strip) {
|
||||
fail(`--strip is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (values.print !== undefined && values.print !== "native-link-info") {
|
||||
fail(`unknown print kind "${values.print}" (supported: native-link-info)\n\n${USAGE}`);
|
||||
}
|
||||
const printNativeLinkInfo = values.print === "native-link-info";
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
if (printNativeLinkInfo && values.emit !== undefined && values.emit !== "obj") {
|
||||
fail(`--print=native-link-info requires --emit=obj\n\n${USAGE}`);
|
||||
}
|
||||
if (externalTypeArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--external-types is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const externalTypes: Record<string, string> = Object.create(null) as Record<string, string>;
|
||||
for (const mapping of externalTypeArgs) {
|
||||
const equals = mapping.indexOf("=");
|
||||
if (equals <= 0 || equals === mapping.length - 1) {
|
||||
fail(`invalid --external-types mapping ${JSON.stringify(mapping)} (expected <specifier=file.d.ts>)`);
|
||||
}
|
||||
const specifier = mapping.slice(0, equals).trim();
|
||||
const declarationArg = mapping.slice(equals + 1).trim();
|
||||
if (!isExactExternalTypeSpecifier(specifier)) {
|
||||
fail(`invalid --external-types specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`);
|
||||
}
|
||||
if (!/\.d\.(?:ts|mts|cts)$/.test(declarationArg)) {
|
||||
fail(`invalid --external-types declaration ${JSON.stringify(declarationArg)} (expected a .d.ts, .d.mts, or .d.cts file)`);
|
||||
}
|
||||
if (externalTypes[specifier] !== undefined) {
|
||||
fail(`duplicate --external-types mapping for ${JSON.stringify(specifier)}`);
|
||||
}
|
||||
const declarationPath = resolve(declarationArg);
|
||||
try {
|
||||
if (!statSync(declarationPath).isFile()) throw new Error("not a file");
|
||||
} catch {
|
||||
fail(`--external-types declaration does not name a readable file: ${declarationPath}`);
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
}
|
||||
const optimization = values.optimization;
|
||||
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") {
|
||||
fail(`unknown optimization "${optimization}" (supported: release, dev)\n\n${USAGE}`);
|
||||
}
|
||||
const windowsSubsystem = values["windows-subsystem"];
|
||||
if (windowsSubsystem !== undefined && windowsSubsystem !== "console" && windowsSubsystem !== "gui") {
|
||||
fail(`unknown Windows subsystem "${windowsSubsystem}" (supported: console, gui)\n\n${USAGE}`);
|
||||
}
|
||||
if (windowsSubsystem !== undefined && command === "coverage") {
|
||||
fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`);
|
||||
}
|
||||
const output = command === "coverage"
|
||||
? null
|
||||
: resolveOutputOptions(command, {
|
||||
...(values.emit === undefined && !printNativeLinkInfo
|
||||
? {}
|
||||
: { emit: values.emit ?? "obj" }),
|
||||
emitIr: values["emit-ir"],
|
||||
...(values.backend === undefined ? {} : { backend: values.backend }),
|
||||
keepLlvm: values["keep-llvm"],
|
||||
sanitize: values.sanitize,
|
||||
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
|
||||
strip: values.strip,
|
||||
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
|
||||
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
|
||||
});
|
||||
if (output !== null && !output.ok) fail(`${output.message}\n\n${USAGE}`);
|
||||
const backend = output?.ok ? output.backend : undefined;
|
||||
|
||||
// --npm-static: repeatable and comma-splittable; the literal "auto"
|
||||
// switches to eligibility-based detection (mixing "auto" with names
|
||||
// is rejected — the shapes answer different questions).
|
||||
const npmStaticRaw = (values["npm-static"] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
||||
let npmStatic: string[] | "auto" | undefined;
|
||||
if (npmStaticRaw.includes("auto")) {
|
||||
if (npmStaticRaw.length > 1) fail(`--npm-static auto cannot be combined with package names\n\n${USAGE}`);
|
||||
npmStatic = "auto";
|
||||
} else if (npmStaticRaw.length > 0) {
|
||||
npmStatic = npmStaticRaw;
|
||||
}
|
||||
|
||||
// --provenance-sources resolves BEFORE the program loads (tsgo needs the
|
||||
// source "paths" at creation): attestations and source trees fetch (or
|
||||
// ride the content-addressed cache / the offline manifest), the registry
|
||||
// installs, and every fallback prints as a note — never a failure.
|
||||
const provenance = values["provenance-sources"] ? await resolveProvenanceSources(input) : null;
|
||||
if (provenance !== null) {
|
||||
setProvenanceSources(provenance);
|
||||
for (const pkg of provenance.packages) {
|
||||
process.stderr.write(
|
||||
`provenance: ${pkg.name}@${pkg.version} ← ${pkg.repo.replace(/^git\+/, "")} @ ${pkg.commit.slice(0, 12)} (source compiles statically)\n`,
|
||||
);
|
||||
}
|
||||
for (const note of provenance.notes) process.stderr.write(`provenance: ${note}\n`);
|
||||
}
|
||||
|
||||
if (command === "coverage") {
|
||||
const { coverage, sourceTexts } = analyze(input, {
|
||||
dynamic: values.dynamic,
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
});
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(renderCoverage(coverage, { color, sourceTexts }) + "\n");
|
||||
return coverage.preflightFailed ? 1 : 0;
|
||||
}
|
||||
|
||||
if (output === null || !output.ok) throw new Error("internal output-option state");
|
||||
if (windowsSubsystem !== undefined && sourceTargetPlatform() !== "win32") {
|
||||
fail(`--windows-subsystem requires a Windows executable target\n\n${USAGE}`);
|
||||
}
|
||||
const { outDir, outPath } = selectOutputPaths(input, output.cliOutputKind, values.out);
|
||||
|
||||
let nativeLinkInfo: object | undefined;
|
||||
const build = async (): Promise<string> => {
|
||||
const result = await compile(input, {
|
||||
outPath,
|
||||
outDir,
|
||||
outputKind: output.outputKind,
|
||||
emitIr: output.emitIr,
|
||||
sanitize: values.sanitize,
|
||||
dynamic: values.dynamic,
|
||||
...(backend !== undefined ? { backend } : {}),
|
||||
...(optimization !== undefined ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true } : {}),
|
||||
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n");
|
||||
const n = result.diagnostics.length;
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
if (result.artifact.kind === "exe") {
|
||||
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
} else if (result.artifact.kind === "obj") {
|
||||
nativeLinkInfo = result.artifact.nativeLinkInfo;
|
||||
}
|
||||
return result.artifact.path;
|
||||
};
|
||||
|
||||
const binary = await build();
|
||||
|
||||
if (command === "run") {
|
||||
return new Promise<number>((resolveExit) => {
|
||||
let child;
|
||||
if (sourceTargetPlatform() === "wasi") {
|
||||
const builtRunner = fileURLToPath(new URL("./wasi-runner.js", import.meta.url));
|
||||
const runner = existsSync(builtRunner)
|
||||
? builtRunner
|
||||
: fileURLToPath(new URL("./wasi-runner.ts", import.meta.url));
|
||||
child = spawn(
|
||||
process.execPath,
|
||||
[...process.execArgv, "--no-warnings", runner, binary],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
} else {
|
||||
child = spawn(binary, [], { stdio: "inherit" });
|
||||
}
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.stderr.write(`scriptc: program killed by ${signal}\n`);
|
||||
resolveExit(1);
|
||||
} else {
|
||||
resolveExit(code ?? 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (printNativeLinkInfo) {
|
||||
if (nativeLinkInfo === undefined) throw new Error("internal native-link-info state");
|
||||
// Keep stdout pure JSON for tooling; the ordinary artifact path is in
|
||||
// program.object inside the document.
|
||||
process.stdout.write(`${JSON.stringify(nativeLinkInfo, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${binary}\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
process.exitCode = await main();
|
||||
} catch (err) {
|
||||
if (err instanceof CliExit) process.exitCode = err.code;
|
||||
else throw err;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,91 +1 @@
|
||||
import type { CompileOutputKind } from "@scriptc/compiler";
|
||||
import type { CliOutputKind } from "./paths.js";
|
||||
|
||||
export interface OutputOptionValues {
|
||||
emit?: string;
|
||||
emitIr: boolean;
|
||||
backend?: string;
|
||||
keepLlvm: boolean;
|
||||
sanitize: boolean;
|
||||
optimization?: string;
|
||||
strip?: boolean;
|
||||
windowsSubsystem?: string;
|
||||
ffi?: string;
|
||||
}
|
||||
|
||||
export type OutputOptionResolution =
|
||||
| {
|
||||
ok: true;
|
||||
outputKind: CompileOutputKind;
|
||||
cliOutputKind: CliOutputKind;
|
||||
backend?: "llvm";
|
||||
emitIr: boolean;
|
||||
deprecateEmitIr: boolean;
|
||||
}
|
||||
| { ok: false; message: string };
|
||||
|
||||
const SOURCE_KINDS = new Set<CliOutputKind>(["ir", "llvm"]);
|
||||
const NATIVE_ARTIFACT_KINDS = new Set<CliOutputKind>(["asm", "obj"]);
|
||||
|
||||
/** Pure compatibility/validation matrix for build/run output selection. */
|
||||
export function resolveOutputOptions(
|
||||
command: "build" | "run",
|
||||
values: OutputOptionValues,
|
||||
): OutputOptionResolution {
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
return { ok: false, message: `unknown backend "${values.backend}" (supported: llvm)` };
|
||||
}
|
||||
const backend = values.backend as "llvm" | undefined;
|
||||
const rawEmit = values.emit;
|
||||
if (
|
||||
rawEmit !== undefined && rawEmit !== "ir" && rawEmit !== "llvm" &&
|
||||
rawEmit !== "asm" && rawEmit !== "obj" && rawEmit !== "exe"
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `unknown emit kind "${rawEmit}" (supported: ir, llvm, asm, obj, exe)`,
|
||||
};
|
||||
}
|
||||
const emit = (rawEmit ?? "exe") as CliOutputKind;
|
||||
if (command === "run" && emit !== "exe") {
|
||||
return { ok: false, message: `scriptc run requires --emit=exe` };
|
||||
}
|
||||
if (values.emitIr && rawEmit !== undefined && emit !== "ir" && emit !== "exe") {
|
||||
return { ok: false, message: `--emit-ir cannot be combined with --emit=${emit}; use --emit=ir` };
|
||||
}
|
||||
if (values.emitIr && emit === "ir") {
|
||||
return { ok: false, message: `--emit-ir and --emit=ir select the same output; use --emit=ir` };
|
||||
}
|
||||
if (values.windowsSubsystem !== undefined && emit !== "exe") {
|
||||
return { ok: false, message: `--windows-subsystem is only supported with --emit=exe` };
|
||||
}
|
||||
if (values.strip && emit !== "exe") {
|
||||
return { ok: false, message: `--strip is only supported with --emit=exe` };
|
||||
}
|
||||
if (emit === "ir" && backend !== undefined) {
|
||||
return { ok: false, message: `--emit=ir cannot be combined with --backend; IR is emitted before backend selection` };
|
||||
}
|
||||
if (SOURCE_KINDS.has(emit)) {
|
||||
if (!values.keepLlvm) {
|
||||
return { ok: false, message: `--no-keep-llvm is only meaningful with --emit=exe` };
|
||||
}
|
||||
if (values.sanitize) {
|
||||
return { ok: false, message: `--sanitize is only meaningful with --emit=exe` };
|
||||
}
|
||||
if (values.optimization !== undefined) {
|
||||
return { ok: false, message: `--optimization is only meaningful with --emit=exe` };
|
||||
}
|
||||
}
|
||||
if (NATIVE_ARTIFACT_KINDS.has(emit) && !values.keepLlvm) {
|
||||
return { ok: false, message: `--no-keep-llvm is only meaningful with --emit=exe` };
|
||||
}
|
||||
const outputKind = emit as CompileOutputKind;
|
||||
return {
|
||||
ok: true,
|
||||
outputKind,
|
||||
cliOutputKind: emit,
|
||||
...(emit === "ir" && backend === undefined ? {} : { backend: "llvm" as const }),
|
||||
emitIr: values.emitIr && emit === "exe",
|
||||
deprecateEmitIr: values.emitIr,
|
||||
};
|
||||
}
|
||||
export * from "@scriptc/compiler/cli/output-options";
|
||||
|
||||
+1
-103
@@ -1,103 +1 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { buildTargetPlatform, sourceTargetPlatform, wasiGuestPath } from "@scriptc/compiler";
|
||||
|
||||
export type CliOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
const POSIX_SUFFIXES: Record<CliOutputKind, string> = {
|
||||
ir: ".ir.json",
|
||||
llvm: ".ll",
|
||||
asm: ".s",
|
||||
obj: ".o",
|
||||
exe: "",
|
||||
};
|
||||
|
||||
const WINDOWS_SUFFIXES: Record<CliOutputKind, string> = {
|
||||
...POSIX_SUFFIXES,
|
||||
asm: ".asm",
|
||||
obj: ".obj",
|
||||
exe: ".exe",
|
||||
};
|
||||
|
||||
export function defaultOutputName(
|
||||
stem: string,
|
||||
kind: CliOutputKind,
|
||||
platform?: string,
|
||||
): string {
|
||||
const selectedPlatform = platform ?? (kind === "exe" ? sourceTargetPlatform() : process.platform);
|
||||
if (kind === "exe" && selectedPlatform === "wasi") return `${stem}.wasm`;
|
||||
return `${stem}${(selectedPlatform === "win32" ? WINDOWS_SUFFIXES : POSIX_SUFFIXES)[kind]}`;
|
||||
}
|
||||
|
||||
export interface OutputPaths {
|
||||
outDir: string;
|
||||
outPath: string;
|
||||
}
|
||||
|
||||
/** One authority for explicit and default primary artifact paths. */
|
||||
export function selectOutputPaths(
|
||||
input: string,
|
||||
kind: CliOutputKind,
|
||||
explicitOut?: string,
|
||||
platform?: string,
|
||||
): OutputPaths {
|
||||
const absoluteInput = resolve(input);
|
||||
const outDir = explicitOut === undefined
|
||||
? join(dirname(absoluteInput), ".scriptc")
|
||||
: dirname(resolve(explicitOut));
|
||||
const stem = basename(absoluteInput).replace(/\.(ts|mts|cts|js|mjs|cjs|c|ll)$/, "");
|
||||
return {
|
||||
outDir,
|
||||
outPath: explicitOut === undefined
|
||||
? join(outDir, defaultOutputName(stem, kind, platform))
|
||||
: resolve(explicitOut),
|
||||
};
|
||||
}
|
||||
|
||||
/** Default executable filename for the build target. Explicit --out paths
|
||||
* stay exact; only scriptc's generated default needs the Windows PE suffix. */
|
||||
export function defaultExecutableName(stem: string, platform: string = buildTargetPlatform()): string {
|
||||
return defaultOutputName(stem, "exe", platform);
|
||||
}
|
||||
|
||||
/** Host paths exposed by `scriptc run` to a WASI Preview 1 module. Guest
|
||||
* `/tmp` maps to the host's real platform temp directory instead of assuming
|
||||
* the POSIX spelling exists (notably false on Windows). */
|
||||
export function wasiPreopens(
|
||||
cwd: string = process.cwd(),
|
||||
hostTmp: string = tmpdir(),
|
||||
): Record<string, string> {
|
||||
return { "/": cwd, "/tmp": hostTmp };
|
||||
}
|
||||
|
||||
/** Environment inherited by a WASI module. Host-absolute directory values
|
||||
* must not claim paths outside the guest namespace: `/` is the module's
|
||||
* capability root/home/cwd and `/tmp` is its writable temporary directory. */
|
||||
export function wasiEnvironment(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
cwd: string = process.cwd(),
|
||||
hostTmp: string = tmpdir(),
|
||||
): Record<string, string> {
|
||||
const guest = Object.fromEntries(
|
||||
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
||||
);
|
||||
|
||||
guest["PWD"] = "/";
|
||||
guest["HOME"] = "/";
|
||||
guest["TMPDIR"] = "/tmp";
|
||||
if (guest["USERPROFILE"] !== undefined) guest["USERPROFILE"] = "/";
|
||||
if (guest["TMP"] !== undefined) guest["TMP"] = "/tmp";
|
||||
if (guest["TEMP"] !== undefined) guest["TEMP"] = "/tmp";
|
||||
|
||||
// These optional shell/package-manager paths retain their meaning only
|
||||
// when they fall under a capability the runner actually exposes.
|
||||
for (const key of ["OLDPWD", "INIT_CWD"] as const) {
|
||||
const value = guest[key];
|
||||
if (value === undefined) continue;
|
||||
const mapped = wasiGuestPath(value, cwd, hostTmp);
|
||||
if (mapped === null) delete guest[key];
|
||||
else guest[key] = mapped;
|
||||
}
|
||||
|
||||
return guest;
|
||||
}
|
||||
export * from "@scriptc/compiler/cli/paths";
|
||||
|
||||
@@ -1,85 +1 @@
|
||||
export const USAGE = `scriptc — TypeScript/JavaScript to native and WebAssembly executables (experimental)
|
||||
|
||||
Usage:
|
||||
scriptc build <file.ts|.js> [options] compile to an executable or source artifact
|
||||
scriptc run <file.ts|.js> [options] compile and run
|
||||
scriptc coverage <file.ts|.js> how much compiles statically, and why not
|
||||
scriptc coverage <file.ts|.js> --dynamic what a --dynamic build compiles, and what still blocks it
|
||||
scriptc coverage <file.ts|.js> --external-types <specifier=file.d.ts>
|
||||
type-resolve an embedder-provided module for analysis
|
||||
scriptc build --lib --profile <p.json> library mode: compile the profile's entry
|
||||
module to a linkable static archive
|
||||
(<name>.lib.a) exporting the
|
||||
profile-declared C symbols; a profile
|
||||
with a sidecar section also gets the
|
||||
contract sidecar JSON beside the archive
|
||||
scriptc cache warm [runtime|tls|dynamic…] prebuild expensive native cache families
|
||||
for the current compiler/SDK/target
|
||||
|
||||
Options:
|
||||
-o, --out <path> primary output path (default: .scriptc/<name><suffix>)
|
||||
--emit <kind> primary output: ir, llvm, asm, obj, or exe
|
||||
(default: exe). asm/obj use the matching platform helper
|
||||
--print <kind> print machine-readable metadata instead of the output path
|
||||
(native-link-info implies --emit=obj and never links)
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
uses -O0, source breakpoints, and cached LLVM object shards;
|
||||
macOS executable builds also produce an adjacent .dSYM
|
||||
--strip remove symbol/debug payload from the linked executable
|
||||
for smaller builds (opt in; --emit=exe only)
|
||||
--windows-subsystem <console|gui>
|
||||
Windows executable subsystem (default: console). gui
|
||||
prevents Windows from opening a console window
|
||||
--keep-llvm keep generated LLVM IR beside the executable (default)
|
||||
--no-keep-llvm delete generated LLVM IR after compiling
|
||||
--emit-ir also write IR beside an executable or library archive;
|
||||
deprecated for executables: use --emit=ir for primary IR
|
||||
--sanitize build with ASan + runtime RC audit
|
||||
--dynamic embed the dynamic engine (adds ~620KB; static stays the default)
|
||||
--ffi <file> bind signature-only TypeScript declarations to native
|
||||
C symbols and link the manifest's archives/libraries
|
||||
--npm-static <pkg[,pkg…]|auto>
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers). A package preflight refuses falls back to the
|
||||
island (--dynamic) with a coverage-report note — opt-in,
|
||||
experimental
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
provenance-attested SOURCE (fetched at the attested
|
||||
commit) as static program modules; packages without a
|
||||
usable attestation keep the island path (a note, never
|
||||
a failure)
|
||||
--external-types <specifier=file.d.ts>
|
||||
coverage only: map an exact bare module specifier to a
|
||||
local declaration file. The declaration supplies types
|
||||
for analysis; the host module remains an explicit
|
||||
external-boundary blocker (repeatable)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
|
||||
export const CLI_OPTIONS = {
|
||||
out: { type: "string", short: "o" },
|
||||
emit: { type: "string" },
|
||||
print: { type: "string" },
|
||||
backend: { type: "string" },
|
||||
optimization: { type: "string" },
|
||||
strip: { type: "boolean", default: false },
|
||||
"windows-subsystem": { type: "string" },
|
||||
"keep-llvm": { type: "boolean", default: true },
|
||||
"emit-ir": { type: "boolean", default: false },
|
||||
sanitize: { type: "boolean", default: false },
|
||||
dynamic: { type: "boolean", default: false },
|
||||
ffi: { type: "string" },
|
||||
"npm-static": { type: "string", multiple: true },
|
||||
"provenance-sources": { type: "boolean", default: false },
|
||||
"external-types": { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
version: { type: "boolean", short: "v", default: false },
|
||||
} as const;
|
||||
export * from "@scriptc/compiler/cli/usage";
|
||||
|
||||
@@ -1,39 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
/* The subprocess host for `scriptc run` on wasm32-wasi. Keeping the WASI
|
||||
* instance outside the CLI process preserves native run's exit isolation:
|
||||
* process.exit(), traps, and signals cannot take the compiler process down.
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { WASI } from "node:wasi";
|
||||
import { wasiGuestPath } from "@scriptc/compiler";
|
||||
import { wasiEnvironment, wasiPreopens } from "./paths.js";
|
||||
|
||||
type WasiInstance = Parameters<WASI["start"]>[0];
|
||||
declare const WebAssembly: {
|
||||
instantiate(
|
||||
bytes: Uint8Array,
|
||||
imports: ReturnType<WASI["getImportObject"]>,
|
||||
): Promise<{ instance: WasiInstance }>;
|
||||
};
|
||||
|
||||
const binary = process.argv[2];
|
||||
if (binary === undefined) throw new Error("scriptc WASI runner needs a module path");
|
||||
const cwd = process.cwd();
|
||||
const hostTmp = tmpdir();
|
||||
|
||||
const wasi = new WASI({
|
||||
version: "preview1",
|
||||
args: [wasiGuestPath(binary, cwd, hostTmp) ?? binary],
|
||||
env: wasiEnvironment(process.env, cwd, hostTmp),
|
||||
// A native scriptc executable inherits access to the caller's filesystem.
|
||||
// WASI is capability-based, so expose the caller's working tree as `/`
|
||||
// and the platform's actual temporary directory as guest `/tmp`.
|
||||
preopens: wasiPreopens(cwd, hostTmp),
|
||||
returnOnExit: true,
|
||||
});
|
||||
const instantiated = await WebAssembly.instantiate(
|
||||
await readFile(binary),
|
||||
wasi.getImportObject(),
|
||||
);
|
||||
process.exitCode = wasi.start(instantiated.instance);
|
||||
import "@scriptc/compiler/cli/wasi-runner";
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { copyFileSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { installNativeCli, nativeCliPackage } from "../scripts/install-native.mjs";
|
||||
import { prepareNativeCommand } from "../scripts/prepare-native.mjs";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-"));
|
||||
roots.push(root);
|
||||
const directory = join(root, "installation");
|
||||
const packageName = "@scriptc/cli-darwin-arm64";
|
||||
const platform = join(directory, "node_modules", packageName);
|
||||
const bin = join(platform, "dist/bin");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeFileSync(join(directory, "package.json"), JSON.stringify({ name: "scriptc", version: "1.2.3" }));
|
||||
writeFileSync(join(platform, "package.json"), JSON.stringify({ name: packageName, version: "1.2.3" }));
|
||||
const binary = join(bin, process.platform === "win32" ? "scriptc.exe" : "scriptc");
|
||||
writeFileSync(binary, "native compiler payload");
|
||||
const toolchain = {
|
||||
schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "macos-arm64",
|
||||
ts7: "../lib/typescript/lib/tsc", llvm_package: "../lib/llvm", runtime_pack: "../lib/runtime",
|
||||
runtime_packs: [{ target: "macos-arm64", path: "../lib/runtime" }],
|
||||
linker: "clang", linker_args: [], dsymutil: "dsymutil", comptime: "../lib/comptime",
|
||||
wasi_node_runner: "../lib/wasi/cli/wasi-runner.js",
|
||||
};
|
||||
writeFileSync(binary + ".json", JSON.stringify(toolchain));
|
||||
const helpers = [toolchain.ts7, toolchain.comptime, join(toolchain.llvm_package, "bin/scriptc-llvm-codegen")]
|
||||
.map((path) => resolve(bin, path));
|
||||
for (const path of helpers) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, "native helper payload", { mode: 0o644 });
|
||||
}
|
||||
return { root, directory, packageName, platform, binary, toolchain, helpers };
|
||||
}
|
||||
|
||||
test("selects only supported native host packages, including Linux libc", () => {
|
||||
expect(nativeCliPackage("darwin", "arm64", null)).toBe("@scriptc/cli-darwin-arm64");
|
||||
expect(nativeCliPackage("win32", "x64", null)).toBe("@scriptc/cli-win32-x64-msvc");
|
||||
expect(nativeCliPackage("linux", "x64", "glibc")).toBe("@scriptc/cli-linux-x64-gnu");
|
||||
expect(nativeCliPackage("linux", "arm64", "musl")).toBe("@scriptc/cli-linux-arm64-musl");
|
||||
expect(() => nativeCliPackage("linux", "x64", null)).toThrow("identify");
|
||||
expect(() => nativeCliPackage("win32", "arm64", null)).toThrow("no native command");
|
||||
});
|
||||
|
||||
test("installs a direct executable and relocatable references to platform assets", () => {
|
||||
const f = fixture();
|
||||
const command = installNativeCli(f.directory, f.packageName);
|
||||
expect(readFileSync(command, "utf8")).toBe("native compiler payload");
|
||||
if (process.platform !== "win32") expect(statSync(command).mode & 0o111).toBe(0o111);
|
||||
if (process.platform !== "win32") {
|
||||
for (const helper of f.helpers) expect(statSync(helper).mode & 0o111).toBe(0o111);
|
||||
}
|
||||
const moved = join(f.root, "relocated");
|
||||
renameSync(f.directory, moved);
|
||||
const installed = join(moved, "bin/scriptc.exe");
|
||||
const manifest = JSON.parse(readFileSync(installed + ".json", "utf8"));
|
||||
const platform = join(moved, "node_modules", f.packageName, "dist/lib");
|
||||
expect(resolve(dirname(installed), manifest.ts7)).toBe(join(platform, "typescript/lib/tsc"));
|
||||
expect(resolve(dirname(installed), manifest.wasi_node_runner)).toBe(join(platform, "wasi/cli/wasi-runner.js"));
|
||||
expect(resolve(dirname(installed), manifest.runtime_packs[0].path)).toBe(join(platform, "runtime"));
|
||||
expect(manifest.linker).toBe("clang");
|
||||
expect(JSON.stringify(manifest)).not.toContain(f.directory);
|
||||
});
|
||||
|
||||
test("missing and mismatched packages fail without replacing an installed command", () => {
|
||||
const f = fixture();
|
||||
const command = installNativeCli(f.directory, f.packageName);
|
||||
writeFileSync(join(f.platform, "package.json"), JSON.stringify({ name: f.packageName, version: "1.2.4" }));
|
||||
expect(() => installNativeCli(f.directory, f.packageName)).toThrow("found 1.2.4");
|
||||
expect(() => installNativeCli(f.directory, "@scriptc/cli-missing")).toThrow("optional dependencies");
|
||||
expect(readFileSync(command, "utf8")).toBe("native compiler payload");
|
||||
});
|
||||
|
||||
test("npm links the installed native executable without an interpreter", () => {
|
||||
const f = fixture();
|
||||
// Keep the package payload small while exercising npm's native shim.
|
||||
// Production command coverage builds the real compiler in the bootstrap gate.
|
||||
const windows = process.platform === "win32";
|
||||
if (windows) copyFileSync(process.execPath, f.binary);
|
||||
else {
|
||||
const source = join(f.root, "payload.c");
|
||||
writeFileSync(source, '#include <stdio.h>\nint main(void) { puts("native compiler"); return 0; }\n');
|
||||
execFileSync("clang", [source, "-o", f.binary]);
|
||||
}
|
||||
for (const helper of f.helpers) writeFileSync(helper, "#!/bin/sh\nprintf 'native helper\\n'\n");
|
||||
const packageName = "scriptc-native-install-test";
|
||||
const scripts = join(f.directory, "scripts");
|
||||
mkdirSync(scripts);
|
||||
copyFileSync(join(import.meta.dirname, "../scripts/install-native.mjs"), join(scripts, "install-native.mjs"));
|
||||
writeFileSync(join(scripts, "fixture-install.mjs"),
|
||||
`import { installNativeCli } from './install-native.mjs'; installNativeCli(process.cwd(), ${JSON.stringify(f.packageName)});\n`);
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const npmEnv = { ...process.env, NODE_PATH: "", npm_config_cache: join(f.root, "npm-cache"), npm_config_update_notifier: "false" };
|
||||
const pack = (directory: string): string => {
|
||||
const result = execFileSync(npm, ["pack", "--json", "--ignore-scripts"], {
|
||||
cwd: directory, env: npmEnv, encoding: "utf8", shell: process.platform === "win32",
|
||||
});
|
||||
return join(directory, (JSON.parse(result) as { filename: string }[])[0]!.filename);
|
||||
};
|
||||
const platformTarball = pack(f.platform);
|
||||
writeFileSync(join(f.directory, "package.json"), JSON.stringify({
|
||||
name: packageName, version: "1.2.3", type: "module",
|
||||
bin: { [packageName]: "bin/scriptc.exe" }, files: ["bin", "scripts"],
|
||||
scripts: { postinstall: "node scripts/fixture-install.mjs" },
|
||||
optionalDependencies: { [f.packageName]: "file:" + platformTarball },
|
||||
}));
|
||||
prepareNativeCommand(f.directory);
|
||||
const tarball = pack(f.directory);
|
||||
const installed = join(f.root, "npm-install");
|
||||
mkdirSync(installed);
|
||||
execFileSync(npm, ["install", "--offline", "--no-audit", "--no-fund", tarball], {
|
||||
cwd: installed, env: npmEnv, shell: process.platform === "win32", stdio: "pipe",
|
||||
});
|
||||
const command = join(installed, "node_modules/.bin", packageName + (process.platform === "win32" ? ".cmd" : ""));
|
||||
const result = execFileSync(command, windows ? ["--version"] : ["native compiler"], {
|
||||
env: { ...process.env, PATH: "" }, encoding: "utf8", shell: process.platform === "win32",
|
||||
});
|
||||
expect(result.trim()).toBe(windows ? process.version : "native compiler");
|
||||
if (process.platform !== "win32") {
|
||||
const bin = join(installed, "node_modules", packageName, "bin");
|
||||
const manifest = JSON.parse(readFileSync(join(bin, "scriptc.exe.json"), "utf8"));
|
||||
for (const helper of [manifest.ts7, manifest.comptime, join(manifest.llvm_package, "bin/scriptc-llvm-codegen")]) {
|
||||
expect(execFileSync(resolve(bin, helper), [], { env: { ...process.env, PATH: "" }, encoding: "utf8" }).trim()).toBe("native helper");
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
@@ -42,7 +42,7 @@ test.runIf(supported)("WASI helper object plus runtime pack builds and runs with
|
||||
})).resolves.toMatchObject({ stdout: "hello world\n" });
|
||||
});
|
||||
|
||||
test.runIf(supported)("switching WASI LLVM and native C builds preserves both translation units", async () => {
|
||||
test.runIf(supported)("switching WASI and native builds retains both executables", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-wasi-native-output-"));
|
||||
dirs.push(dir);
|
||||
const entry = join(dir, "hello.ts");
|
||||
@@ -59,10 +59,12 @@ test.runIf(supported)("switching WASI LLVM and native C builds preserves both tr
|
||||
const llvm = await readFile(join(outDir, "hello.ll"));
|
||||
const wasm = await readFile(join(outDir, "hello.wasm"));
|
||||
await build(["--backend=llvm", "--keep-llvm"], nativeEnv);
|
||||
const c = await readFile(join(outDir, "hello.c"));
|
||||
expect(await readFile(join(outDir, "hello.ll"))).toEqual(llvm);
|
||||
const native = await readFile(join(outDir, process.platform === "win32" ? "hello.exe" : "hello"));
|
||||
const nativeLlvm = await readFile(join(outDir, "hello.ll"));
|
||||
expect(nativeLlvm.equals(llvm)).toBe(false);
|
||||
expect(await readFile(join(outDir, "hello.wasm"))).toEqual(wasm);
|
||||
|
||||
await build([], wasiEnv);
|
||||
expect(await readFile(join(outDir, "hello.c"))).toEqual(c);
|
||||
expect(await readFile(join(outDir, "hello.ll"))).toEqual(llvm);
|
||||
expect(await readFile(join(outDir, process.platform === "win32" ? "hello.exe" : "hello"))).toEqual(native);
|
||||
});
|
||||
|
||||
+2
-3
@@ -1080,9 +1080,8 @@ declare module "node:fs" {
|
||||
): void;
|
||||
/* The bare-encoding spelling — the options record's encoding key alone. */
|
||||
export function writeFileSync(path: string, data: string, encoding: "utf8" | "utf-8"): void;
|
||||
export function writeFileSync(path: string, data: Uint8Array): void;
|
||||
export function appendFileSync(path: string, data: string): void;
|
||||
export function appendFileSync(path: string, data: Uint8Array): void;
|
||||
export function writeFileSync(path: string, data: string | Uint8Array): void;
|
||||
export function appendFileSync(path: string, data: string | Uint8Array): void;
|
||||
export function existsSync(path: string): boolean;
|
||||
export function mkdirSync(path: string): void;
|
||||
export function mkdirSync(path: string, options: { recursive?: boolean; mode?: number }): void;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/* Isolated JavaScript evaluation for comptime callbacks. No filesystem,
|
||||
* process, module loader, or console bindings enter the guest context. */
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
#include "quickjs.h"
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
static uint64_t now_ms(void) {
|
||||
#ifdef _WIN32
|
||||
return GetTickCount64();
|
||||
#else
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
||||
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef struct { uint64_t deadline; int interrupted; } Budget;
|
||||
static int interrupt(JSRuntime *rt, void *opaque) {
|
||||
(void)rt;
|
||||
Budget *budget = opaque;
|
||||
if (now_ms() >= budget->deadline) budget->interrupted = 1;
|
||||
return budget->interrupted;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t length, capacity;
|
||||
void *active[512];
|
||||
size_t depth;
|
||||
} Output;
|
||||
|
||||
static int append(Output *out, const char *text) {
|
||||
size_t size = strlen(text);
|
||||
if (size > 64 * 1024 * 1024 - out->length) return -1;
|
||||
size_t required = out->length + size + 1;
|
||||
if (required > out->capacity) {
|
||||
size_t capacity = out->capacity ? out->capacity : 1024;
|
||||
while (capacity < required) capacity *= 2;
|
||||
char *data = realloc(out->data, capacity);
|
||||
if (!data) return -1;
|
||||
out->data = data;
|
||||
out->capacity = capacity;
|
||||
}
|
||||
memcpy(out->data + out->length, text, size + 1);
|
||||
out->length += size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int quote(JSContext *ctx, Output *out, JSValueConst value) {
|
||||
size_t length;
|
||||
const uint16_t *text = JS_ToCStringLenUTF16(ctx, &length, value);
|
||||
if (!text) return -1;
|
||||
int status = append(out, "\"");
|
||||
for (size_t i = 0; status == 0 && i < length; i++) {
|
||||
char escaped[7];
|
||||
unsigned ch = text[i];
|
||||
if (ch >= 32 && ch < 127 && ch != '"' && ch != '\\') {
|
||||
escaped[0] = (char)ch; escaped[1] = 0;
|
||||
} else snprintf(escaped, sizeof(escaped), "\\u%04x", ch);
|
||||
status = append(out, escaped);
|
||||
}
|
||||
JS_FreeCStringUTF16(ctx, text);
|
||||
return status == 0 ? append(out, "\"") : status;
|
||||
}
|
||||
|
||||
/* Serialize through engine APIs, outside the callback's mutable globals.
|
||||
* The guest cannot replace JSON.stringify or install a toJSON hook that
|
||||
* changes the value the compiler validates. */
|
||||
static int encode(JSContext *ctx, Output *out, JSValueConst value) {
|
||||
if (JS_IsException(value)) return -1;
|
||||
if (JS_IsUndefined(value)) return append(out, "[\"undefined\"]");
|
||||
if (JS_IsNull(value)) return append(out, "[\"null\"]");
|
||||
if (JS_IsBool(value)) return append(out, JS_ToBool(ctx, value) ? "[\"boolean\",true]" : "[\"boolean\",false]");
|
||||
if (JS_IsFunction(ctx, value)) return append(out, "[\"function\"]");
|
||||
if (JS_IsSymbol(value)) return append(out, "[\"symbol\"]");
|
||||
if (JS_IsString(value) || JS_IsNumber(value) || JS_IsBigInt(value)) {
|
||||
const char *tag = JS_IsString(value) ? "[\"string\"," : JS_IsNumber(value) ? "[\"number\"," : "[\"bigint\",";
|
||||
if (append(out, tag) != 0) return -1;
|
||||
double number = 1;
|
||||
if (JS_IsNumber(value) && JS_ToFloat64(ctx, &number, value) < 0) return -1;
|
||||
int status = number == 0 && signbit(number) ? append(out, "\"-0\"") : quote(ctx, out, value);
|
||||
return status == 0 ? append(out, "]") : status;
|
||||
}
|
||||
if (out->depth == 512) { JS_ThrowTypeError(ctx, "compile-time result is nested too deeply"); return -1; }
|
||||
void *identity = JS_VALUE_GET_PTR(value);
|
||||
for (size_t i = 0; i < out->depth; i++) {
|
||||
if (out->active[i] == identity) { JS_ThrowTypeError(ctx, "cyclic compile-time result"); return -1; }
|
||||
}
|
||||
out->active[out->depth++] = identity;
|
||||
int status = 0;
|
||||
if (JS_IsArray(value)) {
|
||||
int64_t length = 0;
|
||||
if (JS_GetLength(ctx, value, &length) < 0 || length < 0 || length > 1000000) status = -1;
|
||||
if (status == 0) status = append(out, "[\"array\",[");
|
||||
for (int64_t i = 0; status == 0 && i < length; i++) {
|
||||
if (i != 0) status = append(out, ",");
|
||||
JSValue item = JS_GetPropertyUint32(ctx, value, (uint32_t)i);
|
||||
if (status == 0) status = encode(ctx, out, item);
|
||||
JS_FreeValue(ctx, item);
|
||||
}
|
||||
} else {
|
||||
JSPropertyEnum *properties = NULL;
|
||||
uint32_t length = 0;
|
||||
status = JS_GetOwnPropertyNames(ctx, &properties, &length, value, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
|
||||
if (status == 0) status = append(out, "[\"object\",[");
|
||||
for (uint32_t i = 0; status == 0 && i < length; i++) {
|
||||
if (i != 0) status = append(out, ",");
|
||||
if (status == 0) status = append(out, "[");
|
||||
JSValue key = JS_AtomToString(ctx, properties[i].atom);
|
||||
if (status == 0) status = quote(ctx, out, key);
|
||||
JS_FreeValue(ctx, key);
|
||||
if (status == 0) status = append(out, ",");
|
||||
JSValue item = JS_GetProperty(ctx, value, properties[i].atom);
|
||||
if (status == 0) status = encode(ctx, out, item);
|
||||
JS_FreeValue(ctx, item);
|
||||
if (status == 0) status = append(out, "]");
|
||||
}
|
||||
JS_FreePropertyEnum(ctx, properties, length);
|
||||
}
|
||||
out->depth--;
|
||||
return status == 0 ? append(out, "]]") : status;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) { fputs("usage: scriptc-comptime <javascript> <timeout-ms>\n", stderr); return 2; }
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long timeout = strtol(argv[2], &end, 10);
|
||||
if (errno || !end || *end || timeout < 1 || timeout > 60000) {
|
||||
fputs("invalid compile-time budget\n", stderr); return 2;
|
||||
}
|
||||
FILE *file = fopen(argv[1], "rb");
|
||||
if (!file) { perror("compile-time input"); return 2; }
|
||||
if (fseek(file, 0, SEEK_END) != 0) { fclose(file); return 2; }
|
||||
long size = ftell(file);
|
||||
if (size < 0 || size > 16 * 1024 * 1024 || fseek(file, 0, SEEK_SET) != 0) { fclose(file); return 2; }
|
||||
char *source = malloc((size_t)size + 1);
|
||||
if (!source) { fclose(file); return 2; }
|
||||
if (fread(source, 1, (size_t)size, file) != (size_t)size) { free(source); fclose(file); return 2; }
|
||||
fclose(file);
|
||||
source[size] = 0;
|
||||
JSRuntime *rt = JS_NewRuntime();
|
||||
if (!rt) { free(source); return 2; }
|
||||
JS_SetMemoryLimit(rt, 256 * 1024 * 1024);
|
||||
JS_SetMaxStackSize(rt, 2 * 1024 * 1024);
|
||||
Budget budget = { now_ms() + (uint64_t)timeout, 0 };
|
||||
JS_SetInterruptHandler(rt, interrupt, &budget);
|
||||
JSContext *ctx = JS_NewContext(rt);
|
||||
if (!ctx) { JS_FreeRuntime(rt); free(source); return 2; }
|
||||
JSValue result = JS_Eval(ctx, source, (size_t)size, "comptime", JS_EVAL_TYPE_GLOBAL);
|
||||
free(source);
|
||||
int status = 0;
|
||||
Output out = {0};
|
||||
if (JS_IsException(result) || encode(ctx, &out, result) != 0) {
|
||||
free(out.data);
|
||||
memset(&out, 0, sizeof(out));
|
||||
JSValue exception = JS_GetException(ctx);
|
||||
if (budget.interrupted) status = append(&out, "[\"error\",\"ERR_SCRIPT_EXECUTION_TIMEOUT\",\"compile-time evaluation timed out\"]");
|
||||
else {
|
||||
JSValue message = JS_GetPropertyStr(ctx, exception, "message");
|
||||
JSValue detail = JS_IsUndefined(message) ? JS_IsNull(exception)
|
||||
? JS_NewString(ctx, "compile-time result exceeds the output limit") : JS_DupValue(ctx, exception) : JS_DupValue(ctx, message);
|
||||
status = append(&out, "[\"error\",\"\",");
|
||||
if (status == 0) status = quote(ctx, &out, detail);
|
||||
if (status == 0) status = append(&out, "]");
|
||||
JS_FreeValue(ctx, detail);
|
||||
JS_FreeValue(ctx, message);
|
||||
}
|
||||
JS_FreeValue(ctx, exception);
|
||||
}
|
||||
if (status == 0 && (fwrite(out.data, 1, out.length, stdout) != out.length || fputc('\n', stdout) == EOF)) status = 2;
|
||||
free(out.data);
|
||||
JS_FreeValue(ctx, result);
|
||||
JS_FreeContext(ctx);
|
||||
JS_FreeRuntime(rt);
|
||||
return status == 0 ? 0 : 2;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* Accept only directories owned by this user with private POSIX access.
|
||||
* Windows inherits the user's cache ACL; reject reparse points so an
|
||||
* installed cache directory cannot redirect compiler payloads elsewhere. */
|
||||
uint8_t scriptc_native_private_directory(const uint8_t *bytes, size_t size, uint8_t harden) {
|
||||
if (size == 0 || memchr(bytes, 0, size) != NULL) return 0;
|
||||
#ifdef _WIN32
|
||||
(void)harden;
|
||||
if (size > INT_MAX) return 0;
|
||||
int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (const char *)bytes, (int)size, NULL, 0);
|
||||
if (length <= 0) return 0;
|
||||
wchar_t *path = malloc(((size_t)length + 1) * sizeof(wchar_t));
|
||||
if (!path) return 0;
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (const char *)bytes, (int)size, path, length) != length) { free(path); return 0; }
|
||||
path[length] = 0;
|
||||
DWORD attrs = GetFileAttributesW(path);
|
||||
free(path);
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0 && (attrs & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
|
||||
#else
|
||||
char *path = malloc(size + 1);
|
||||
if (!path) return 0;
|
||||
memcpy(path, bytes, size);
|
||||
path[size] = 0;
|
||||
struct stat info;
|
||||
int ok = lstat(path, &info) == 0 && S_ISDIR(info.st_mode) && info.st_uid == getuid();
|
||||
if (ok && harden && (info.st_mode & 077) != 0) ok = chmod(path, 0700) == 0;
|
||||
if (ok) ok = lstat(path, &info) == 0 && S_ISDIR(info.st_mode) && info.st_uid == getuid() && (info.st_mode & 077) == 0;
|
||||
free(path);
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" }
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,13 @@
|
||||
"./scriptc.d.ts": "./ambient/scriptc.d.ts",
|
||||
"./scriptc-overrides.d.ts": "./ambient/scriptc-overrides.d.ts",
|
||||
"./scriptc-node-fallback.d.ts": "./ambient/scriptc-node-fallback.d.ts",
|
||||
"./surface-manifest.json": "./surface-manifest.json"
|
||||
"./surface-manifest.json": "./surface-manifest.json",
|
||||
"./cli/command": "./dist/cli/command.js",
|
||||
"./cli/host": "./dist/cli/host.js",
|
||||
"./cli/paths": "./dist/cli/paths.js",
|
||||
"./cli/output-options": "./dist/cli/output-options.js",
|
||||
"./cli/usage": "./dist/cli/usage.js",
|
||||
"./cli/wasi-runner": "./dist/cli/wasi-runner.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { resolveBuildCacheRoot } from "./cache-root.js";
|
||||
export { resolveBuildCacheRoot } from "./cache-root.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, copyFile, mkdir, readdir, readFile, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
export async function fileExists(path: string): Promise<boolean> {
|
||||
return stat(path).then(
|
||||
@@ -44,28 +45,6 @@ export async function installArtifact(
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the build cache without touching the filesystem. Exported from this
|
||||
* internal module so its platform and override behavior can be pinned directly. */
|
||||
export function resolveBuildCacheRoot(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
userHome: string = homedir(),
|
||||
): string | null {
|
||||
if (env["SCRIPTC_NO_CACHE"] === "1") return null;
|
||||
const configured = env["SCRIPTC_CACHE_DIR"];
|
||||
if (configured !== undefined) return configured === "" ? null : resolve(configured);
|
||||
|
||||
const xdg = env["XDG_CACHE_HOME"];
|
||||
if (xdg !== undefined && xdg !== "") return resolve(xdg, "scriptc", "build");
|
||||
if (platform === "win32") {
|
||||
const local = env["LOCALAPPDATA"];
|
||||
if (local !== undefined && local !== "") return resolve(local, "scriptc", "cache", "build");
|
||||
}
|
||||
return platform === "darwin"
|
||||
? resolve(userHome, "Library", "Caches", "scriptc", "build")
|
||||
: resolve(userHome, ".cache", "scriptc", "build");
|
||||
}
|
||||
|
||||
/** Shared persistent-cache root for compiler-level tiers. The early library
|
||||
* cache deliberately follows the native cache's activation and hard-disable
|
||||
* contract, while native compilation retains ownership of toolchain safety. */
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { homedir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
/** Resolve the build cache without touching the filesystem. Exported from this
|
||||
* internal module so its platform and override behavior can be pinned directly. */
|
||||
export function resolveBuildCacheRoot(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
userHome: string = homedir(),
|
||||
): string | null {
|
||||
if (env["SCRIPTC_NO_CACHE"] === "1") return null;
|
||||
const configured = env["SCRIPTC_CACHE_DIR"];
|
||||
if (configured !== undefined) return configured === "" ? null : resolve(configured);
|
||||
|
||||
const xdg = env["XDG_CACHE_HOME"];
|
||||
if (xdg !== undefined && xdg !== "") return resolve(xdg, "scriptc", "build");
|
||||
if (platform === "win32") {
|
||||
const local = env["LOCALAPPDATA"];
|
||||
if (local !== undefined && local !== "") return resolve(local, "scriptc", "cache", "build");
|
||||
}
|
||||
return platform === "darwin"
|
||||
? resolve(userHome, "Library", "Caches", "scriptc", "build")
|
||||
: resolve(userHome, ".cache", "scriptc", "build");
|
||||
}
|
||||
@@ -5,22 +5,23 @@ import { basename, dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { installBytes } from "../library/cache-primitives.js";
|
||||
import { emitNativeArtifact } from "./native-codegen.js";
|
||||
import { linkNativeExecutable } from "./linker.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { isZigDriver, localizeLibraryObjects, type CcDriver, type LibArchiveOptions } from "./native-toolchain.js";
|
||||
import { loadRuntimePack, RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
import { executableOptimizationLinkerArgs, type NativeTargetSpec } from "./targets.js";
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
/** Assemble a library from LLVM program objects and verified runtime objects.
|
||||
* The runtime's library and per-thread modes are compiled when packaging. */
|
||||
export async function compilePackedLibrary(options: LibArchiveOptions, target: NativeTargetSpec): Promise<void> {
|
||||
export async function compilePackedLibrary(options: LibArchiveOptions, target: NativeTargetSpec, wasmExports?: readonly string[]): Promise<void> {
|
||||
const optimization = options.optimization ?? "release";
|
||||
const features: NativeLinkFeatures = {
|
||||
dynamic: false, regex: options.regex ?? false, copying: options.copying ?? false,
|
||||
textDecoderLegacy: options.textDecoderLegacy ?? false, fileHandle: false, fetch: false,
|
||||
netIsland: false, zlib: options.zlib ?? false, assert: options.assert ?? false,
|
||||
inspect: options.inspect ?? false, dynInvoke: false, dc: false, dynAsync: false,
|
||||
inspect: options.inspect ?? false, dynInvoke: options.dynInvoke ?? false, dc: false, dynAsync: false,
|
||||
events: false, emitter: options.emitter ?? false, symbol: options.symbol ?? false,
|
||||
bigint: options.bigint ?? false, searchParams: options.searchParams ?? false,
|
||||
qs: false, parseArgs: false, stream: false, net: false, http: false, http2: false,
|
||||
@@ -30,6 +31,28 @@ export async function compilePackedLibrary(options: LibArchiveOptions, target: N
|
||||
target, features, optimization,
|
||||
mode: options.threadInstances ? "library-thread" : "library",
|
||||
});
|
||||
if (target.platform === "wasi") {
|
||||
if (wasmExports === undefined) throw new Error("Wasm library linking requires explicit exports");
|
||||
const buildDir = await mkdtemp(join(tmpdir(), "scriptc-wasm-library-"));
|
||||
try {
|
||||
const program = join(buildDir, "program.o");
|
||||
await emitNativeArtifact({
|
||||
outputPath: program, llvm: await readFile(options.cPath, "utf8"), sourcePath: options.cPath,
|
||||
outputKind: "obj", optimization: optimization === "dev" ? "0" : "2", target,
|
||||
});
|
||||
await mkdir(dirname(options.outPath), { recursive: true });
|
||||
await linkNativeExecutable({
|
||||
target, outputPath: options.outPath,
|
||||
inputs: [program, ...selection.runtimeObjects, ...selection.archives],
|
||||
systemLibraries: selection.systemLibraries,
|
||||
driverFlags: ["-target", target.linkerTargetTriple, "-mexec-model=reactor", ...executableOptimizationLinkerArgs(target.platform, optimization), ...wasmExports.map((name) => `-Wl,--export=${name}`)],
|
||||
dependencyPaths: selection.dependencyPaths, programObjectDependencies: [], runtimePack: selection,
|
||||
});
|
||||
} finally {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const stage = await stageRuntimePackArtifacts(selection);
|
||||
try {
|
||||
const buildDir = await mkdtemp(join(tmpdir(), "scriptc-library-pack-"));
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function linkTraceCandidate(line: string): string[] {
|
||||
const trimmed = line.trim().replace(/^(?:LOAD|load)\s+/, "");
|
||||
if (trimmed === "") return [];
|
||||
const unquoted =
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
? trimmed.slice(1, -1)
|
||||
: trimmed;
|
||||
const candidates = [unquoted];
|
||||
const member = unquoted.lastIndexOf("(");
|
||||
if (member > 0 && unquoted.endsWith(")")) candidates.push(unquoted.slice(0, member));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function driverTraceCandidates(line: string): string[] {
|
||||
const candidates: string[] = [];
|
||||
for (const match of line.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) {
|
||||
const token = (match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["\\])/g, "$1");
|
||||
if (token !== "") candidates.push(token);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export interface DynHost extends WalkerHost {
|
||||
unitInstanceRef(unionId: string, tag: number): string;
|
||||
liveDynRefAdapter(t: IrType): { snapshot: string; commit: string };
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
@@ -326,6 +327,29 @@ export class LlDyn {
|
||||
|
||||
/* ── dynMatchHelper (walkers.ts, ported) ──────────────────────── */
|
||||
|
||||
/** Native subclass capsules also satisfy a checked base-class slot.
|
||||
* Class layouts share their base prefix, so unboxing retains the same
|
||||
* pointer and virtual dispatch still uses the object's original vtable. */
|
||||
private typedRefMatches(B: BlockBuilder, t: IrType): string {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${this.S})`);
|
||||
const types: IrType[] = isDynTypedRefType(t)
|
||||
? this.host.classSubtypes(t.className).map((className) => ({ kind: "object", className }))
|
||||
: [t];
|
||||
let matched = "false";
|
||||
for (const type of types) {
|
||||
const key = typeKey(type);
|
||||
const next = B.tmp();
|
||||
B.line(`${next} = call zeroext i1 @scr_dyn_typed_ref_is(ptr %d, ptr ${this.host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")})`);
|
||||
if (matched === "false") matched = next;
|
||||
else {
|
||||
const either = B.tmp();
|
||||
B.line(`${either} = or i1 ${matched}, ${next}`);
|
||||
matched = either;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
||||
dynMatchHelper(t: IrType): string {
|
||||
const key = typeKey(t);
|
||||
@@ -335,11 +359,7 @@ export class LlDyn {
|
||||
this.dynMatchers.set(key, name);
|
||||
const B = new BlockBuilder();
|
||||
if (isRefCounted(t) && t.kind !== "dyn") {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${this.S})`);
|
||||
const matched = B.tmp();
|
||||
B.line(
|
||||
`${matched} = call zeroext i1 @scr_dyn_typed_ref_is(ptr %d, ptr ${this.host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")})`,
|
||||
);
|
||||
const matched = this.typedRefMatches(B, t);
|
||||
const lRef = B.newLabel("dm.tr");
|
||||
const lNext = B.newLabel("dm.nt");
|
||||
B.condBr(matched, lRef, lNext);
|
||||
@@ -659,12 +679,8 @@ export class LlDyn {
|
||||
return name;
|
||||
}
|
||||
if (isRefCounted(t) && t.kind !== "dyn") {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${host.sizeType})`);
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`);
|
||||
const matched = B.tmp();
|
||||
B.line(
|
||||
`${matched} = call zeroext i1 @scr_dyn_typed_ref_is(ptr %d, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")})`,
|
||||
);
|
||||
const matched = this.typedRefMatches(B, t);
|
||||
const lRef = B.newLabel("dc.tr");
|
||||
const lNext = B.newLabel("dc.nt");
|
||||
B.condBr(matched, lRef, lNext);
|
||||
|
||||
@@ -103,6 +103,24 @@ test("tag tests read an owned local without adding a temporary owner", () => {
|
||||
expect(llvm).toContain("icmp eq i32");
|
||||
});
|
||||
|
||||
test("class brand probes borrow a local only across an inert literal key", () => {
|
||||
const dyn: IrType = { kind: "dyn" };
|
||||
const key: IrExpr = { kind: "strLit", value: "object:Example", type: STRING, loc: receiverLoc };
|
||||
const probe: IrExpr = { kind: "libCall", fn: "dyn.typedRefIs", args: [ref(dyn), key], type: BOOL, loc: receiverLoc };
|
||||
const borrowed = work(probe, dyn);
|
||||
expect(borrowed).not.toContain("call ptr @scr_dyn_retain_v(");
|
||||
expect(borrowed).toContain("icmp ne ptr");
|
||||
expect(borrowed).toContain("icmp eq i32");
|
||||
expect(borrowed).toContain("@scr_dyn_typed_ref_is_key");
|
||||
const boxed = work(probe, dyn, true);
|
||||
expect(boxed).toContain("call ptr @scr_box_get_ref");
|
||||
expect(boxed).toContain("call void @scr_dyn_release");
|
||||
const computed = work({ ...probe, args: [ref(dyn), {
|
||||
kind: "toString", operand: ref(dyn), type: STRING, loc: receiverLoc,
|
||||
}] }, dyn);
|
||||
expect(computed).toContain("call ptr @scr_dyn_retain_v(");
|
||||
});
|
||||
|
||||
test("capture boxes keep their ordinary owned-read contract", () => {
|
||||
const llvm = work(text(child(narrow(ref(union)))), union, true);
|
||||
expect(llvm).toContain("call ptr @scr_box_get_ref");
|
||||
|
||||
@@ -360,9 +360,9 @@ export class LlEmitter {
|
||||
this.wasi = options.wasi === true;
|
||||
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
|
||||
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
|
||||
// ScrCycHdr is { ptr trace; ptr free; i32 color; i16 buffered;
|
||||
// i16 gen; size_t buf_index }. The object follows it, so color is 12
|
||||
// bytes behind a wasm32 object and 16 bytes behind a 64-bit object.
|
||||
// ScrCycHdr.color stays 12 bytes behind a wasm32 object and 16 bytes
|
||||
// behind a 64-bit object. The wasm32 header pads before color so its
|
||||
// payload remains double-aligned without changing this ABI offset.
|
||||
this.cycleColorOffset = options.pointerBits === 32 ? 12 : 16;
|
||||
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
|
||||
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
|
||||
@@ -400,6 +400,11 @@ export class LlEmitter {
|
||||
unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag),
|
||||
liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
|
||||
isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error",
|
||||
classSubtypes: (name) => {
|
||||
const target = this.classMetaOf(name);
|
||||
return [...this.classMeta.values()].filter((meta) =>
|
||||
target.pre <= meta.pre && meta.pre <= target.post).map((meta) => meta.def.name);
|
||||
},
|
||||
};
|
||||
this.walkers = new LlWalkers(this.shapeHost);
|
||||
this.dyn = new LlDyn(this.shapeHost);
|
||||
@@ -578,7 +583,7 @@ export class LlEmitter {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
defs.push(...dispatchBody);
|
||||
for (const line of dispatchBody) defs.push(line);
|
||||
|
||||
this.declare(`declare ptr @scr_ffi_call_new(ptr, ptr, ptr, ${this.sizeType})`);
|
||||
this.declare(`declare void @scr_ffi_post(ptr)`);
|
||||
@@ -1029,7 +1034,7 @@ export class LlEmitter {
|
||||
// finish_top_level initially notes 13. Replace that hint before exit
|
||||
// listeners run when a higher-priority verdict was already selected.
|
||||
lines.push(` call void @scr_exit_code_note(i32 ${exitStatus})`);
|
||||
lines.push(...exitListenerLines("xp"));
|
||||
for (const line of exitListenerLines("xp")) lines.push(line);
|
||||
if (tracksIslandExit) {
|
||||
lines.push(
|
||||
` %tla_exit_version_after = call ${this.sizeType} @scr_island_exit_code_version()`,
|
||||
@@ -1045,13 +1050,17 @@ export class LlEmitter {
|
||||
` %tla_final_exit = phi i32 [ %tla_listener_exit, %tla_exit_updated ], [ ${exitStatus}, %tla_exit_unchanged ]`,
|
||||
);
|
||||
}
|
||||
lines.push(...topPendingReleases);
|
||||
for (const line of topPendingReleases) lines.push(line);
|
||||
lines.push(` ret i32 ${tracksIslandExit ? "%tla_final_exit" : exitStatus}`);
|
||||
return lines;
|
||||
};
|
||||
// LIBRARY mode: the runtime entry points the generated library
|
||||
// symbols delegate to — declared before the extern block flushes.
|
||||
if (this.mod.lib !== undefined) {
|
||||
if (this.wasi) {
|
||||
this.declare(`declare ptr @malloc(${this.sizeType})`);
|
||||
this.declare(`declare void @free(ptr)`);
|
||||
}
|
||||
this.declare(`declare void @scr_library_entry(i1 zeroext, ptr)`);
|
||||
this.declare(`declare void @scr_library_reset()`);
|
||||
this.declare(`declare void @scr_library_check_exc()`);
|
||||
@@ -1145,8 +1154,8 @@ export class LlEmitter {
|
||||
`%ScrIslandModule = type { ptr, ptr, ${this.sizeType}, ${this.sizeType}, i32, ptr, ${this.sizeType}, ${this.sizeType} }`,
|
||||
`%ScrIslandEdge = type { ptr, ptr, ptr, i32 }`,
|
||||
];
|
||||
out.push(...shapes.typeDefs);
|
||||
out.push(...classShapes.typeDefs);
|
||||
for (const line of shapes.typeDefs) out.push(line);
|
||||
for (const line of classShapes.typeDefs) out.push(line);
|
||||
// Thread-instanced library state (abi.instance_per_thread): the
|
||||
// program TU's mutable globals — module globals, run-once guards, the
|
||||
// lazily-compiled regex literal caches — and the runtime globals its
|
||||
@@ -1291,7 +1300,7 @@ export class LlEmitter {
|
||||
}
|
||||
out.push(``);
|
||||
}
|
||||
out.push(...ffiCallbacks.globals);
|
||||
for (const line of ffiCallbacks.globals) out.push(line);
|
||||
if (ffiCallbacks.globals.length > 0) out.push(``);
|
||||
for (const g of globals) {
|
||||
const ty = this.llType(g.type);
|
||||
@@ -1300,16 +1309,16 @@ export class LlEmitter {
|
||||
out.push(`@${mangleGlobal(g.id)} = internal ${tl}global ${ty} ${zero}${debug ? `, !dbg ${debug}` : ""} ; ${g.name}`);
|
||||
}
|
||||
if (globals.length > 0) out.push(``);
|
||||
out.push(...helpers);
|
||||
out.push(...ffiCallbacks.defs);
|
||||
out.push(...shapes.defs);
|
||||
out.push(...classShapes.defs);
|
||||
out.push(...classObjDefs);
|
||||
out.push(...this.walkers.defs);
|
||||
out.push(...this.dyn.defs);
|
||||
out.push(...wrappers);
|
||||
out.push(...asyncDefs);
|
||||
out.push(...this.resolveThunkDefs);
|
||||
for (const line of helpers) out.push(line);
|
||||
for (const line of ffiCallbacks.defs) out.push(line);
|
||||
for (const line of shapes.defs) out.push(line);
|
||||
for (const line of classShapes.defs) out.push(line);
|
||||
for (const line of classObjDefs) out.push(line);
|
||||
for (const line of this.walkers.defs) out.push(line);
|
||||
for (const line of this.dyn.defs) out.push(line);
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of this.resolveThunkDefs) out.push(line);
|
||||
out.push(fnDefs.join("\n\n"), ``);
|
||||
|
||||
// main(): scr_init, the program-dependent error-vt interval stamps,
|
||||
@@ -1365,7 +1374,7 @@ export class LlEmitter {
|
||||
if (this.mod.lib !== undefined) {
|
||||
// LIBRARY mode: no @main — the profile-declared external
|
||||
// symbols specified by the library IR instead.
|
||||
out.push(...this.emitLibDefs(globals, globalReleaseLines, stamps));
|
||||
for (const line of this.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line);
|
||||
out.push(`attributes #0 = { sanitize_address }`);
|
||||
if (this.wasi) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`);
|
||||
if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`);
|
||||
@@ -1542,6 +1551,23 @@ export class LlEmitter {
|
||||
if (lib.collectSymbol !== null) emitSymConst(lib.collectSymbol);
|
||||
for (const e of lib.exports) emitSymConst(e.symbol);
|
||||
out.push(``);
|
||||
if (this.wasi) {
|
||||
for (const symbol of ["scriptc_alloc", "scriptc_free"]) emitSymConst(symbol);
|
||||
out.push(
|
||||
`define ptr @scriptc_alloc(i32 %size) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_alloc")})`,
|
||||
` %p = call ptr @malloc(i32 %size)`,
|
||||
` ret ptr %p`,
|
||||
`}`, ``,
|
||||
`define void @scriptc_free(ptr %p) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` call void @scr_library_entry(i1 zeroext false, ptr ${symConst("scriptc_free")})`,
|
||||
` call void @free(ptr %p)`,
|
||||
` ret void`,
|
||||
`}`, ``,
|
||||
);
|
||||
}
|
||||
// The runtime detected-trap overlay table (scr_runtime.h declares it,
|
||||
// the library trap funnel consults it): flat code/teaching/remediation
|
||||
// triples, one per runtime trap code (SC4013–SC4019) the profile
|
||||
@@ -1642,7 +1668,7 @@ export class LlEmitter {
|
||||
// from the poisoned guard and every runtime touch (ratified), so a
|
||||
// host can read them before init and after a trap. The u64 rides
|
||||
// i64 two's-complement (LLVM integer constants are signed).
|
||||
out.push(...emitLibraryIdentityLines(lib.identity, FN_ATTRS));
|
||||
for (const line of emitLibraryIdentityLines(lib.identity, FN_ATTRS)) out.push(line);
|
||||
}
|
||||
if (lib.resultResetSymbol !== null) {
|
||||
out.push(
|
||||
@@ -1987,7 +2013,7 @@ export class LlEmitter {
|
||||
if (fn.async !== true || fn.generator !== undefined) continue;
|
||||
const { definitions, ret, tr, spawnParams, argPackLines } =
|
||||
this.emitArgPackAndTrampolinePrologue(fn);
|
||||
out.push(...definitions);
|
||||
for (const line of definitions) out.push(line);
|
||||
this.declare(`declare ptr @scr_fiber_promise(ptr)`);
|
||||
this.declare(`declare ptr @scr_async_spawn(ptr, ptr)`);
|
||||
this.needOom();
|
||||
@@ -2039,7 +2065,7 @@ export class LlEmitter {
|
||||
}
|
||||
tr.push(` ret void`, `}`, ``);
|
||||
}
|
||||
out.push(...tr);
|
||||
for (const line of tr) out.push(line);
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), spawn the fiber.
|
||||
const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null;
|
||||
@@ -2109,9 +2135,9 @@ export class LlEmitter {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
out.push(...sp);
|
||||
for (const line of sp) out.push(line);
|
||||
}
|
||||
out.push(...this.emitGenScaffolding());
|
||||
for (const line of this.emitGenScaffolding()) out.push(line);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -2135,7 +2161,7 @@ export class LlEmitter {
|
||||
spawnParams,
|
||||
argPackLines,
|
||||
} = this.emitArgPackAndTrampolinePrologue(fn);
|
||||
out.push(...definitions);
|
||||
for (const line of definitions) out.push(line);
|
||||
this.declare(`declare zeroext i1 @scr_exc_genret_pending()`);
|
||||
this.declare(`declare void @scr_exc_clear()`);
|
||||
this.declare(`declare ptr @scr_gen_of_fiber(ptr)`);
|
||||
@@ -2192,7 +2218,7 @@ export class LlEmitter {
|
||||
}
|
||||
tr.push(` br label %done`, `done:`, ` ret void`, `}`, ``);
|
||||
}
|
||||
out.push(...tr);
|
||||
for (const line of tr) out.push(line);
|
||||
|
||||
let settleAsync: string | null = null;
|
||||
if (fn.async) {
|
||||
@@ -2245,7 +2271,7 @@ export class LlEmitter {
|
||||
}
|
||||
});
|
||||
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
|
||||
out.push(...dr);
|
||||
for (const line of dr) out.push(line);
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), allocate the
|
||||
// SUSPENDED fiber — nothing runs until the first .next().
|
||||
@@ -2265,7 +2291,7 @@ export class LlEmitter {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
out.push(...sp);
|
||||
for (const line of sp) out.push(line);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -3789,10 +3815,11 @@ export class LlEmitter {
|
||||
B.line(`store ${this.sizeType} 0, ptr ${integerSlot}`);
|
||||
this.integerLoopBindings.set(integerLoop.localId, integerSlot);
|
||||
} else if (s.init) {
|
||||
// A multi-declarator head's locals belong to the loop scope,
|
||||
// not a temporary block that would release them before condition.
|
||||
if (s.init.kind === "block") for (const init of s.init.body) this.emitStmt(init);
|
||||
else this.emitStmt(s.init);
|
||||
// A multi-declarator head shares the loop's scope. Emitting its
|
||||
// IR block as an ordinary block would release captured/ref locals
|
||||
// before the first condition.
|
||||
const initializers = s.init.kind === "block" ? s.init.body : [s.init];
|
||||
for (const initializer of initializers) this.emitStmt(initializer);
|
||||
}
|
||||
const lc = B.newLabel("loop.c");
|
||||
const lb = B.newLabel("loop.b");
|
||||
@@ -3800,16 +3827,15 @@ export class LlEmitter {
|
||||
// JS `for (let i ...)`: each iteration gets a FRESH binding holding
|
||||
// a copy of the previous one (closures made in iteration k keep
|
||||
// seeing iteration k's value) — only observable, and only emitted,
|
||||
// when the init variable is captured (boxed). The freshening (and
|
||||
// the update) live in the continue-target block.
|
||||
// when a let variable is captured (boxed). Freshen before the first
|
||||
// condition and again in the continue-target block before updating.
|
||||
const initializers = s.init?.kind === "block" ? s.init.body : s.init ? [s.init] : [];
|
||||
const captured = initializers.flatMap((init) => {
|
||||
const capturedLets = initializers.flatMap((init) => {
|
||||
const local = init.kind === "varDecl" ? this.currentLocals.get(init.localId) : undefined;
|
||||
return local?.boxed === true ? [local] : [];
|
||||
return local?.boxed && local.mutable ? [local] : [];
|
||||
});
|
||||
const freshens = captured.length > 0;
|
||||
const freshen = (): void => {
|
||||
for (const local of captured) {
|
||||
const freshenBindings = (): void => {
|
||||
for (const local of capturedLets) {
|
||||
const slot = `%${mangleLocal(local.id)}`;
|
||||
const fresh = B.tmp();
|
||||
const old = B.tmp();
|
||||
@@ -3822,8 +3848,8 @@ export class LlEmitter {
|
||||
B.line(`store ptr ${fresh}, ptr ${slot}`);
|
||||
}
|
||||
};
|
||||
const lu = s.update || freshens ? B.newLabel("loop.u") : lc;
|
||||
freshen();
|
||||
const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
|
||||
freshenBindings();
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
if (integerLoop && integerSlot) {
|
||||
@@ -3854,7 +3880,7 @@ export class LlEmitter {
|
||||
B.br(lu);
|
||||
if (lu !== lc) {
|
||||
B.startBlock(lu);
|
||||
freshen();
|
||||
freshenBindings();
|
||||
if (integerLoop && integerSlot) {
|
||||
const old = B.tmp();
|
||||
const next = B.tmp();
|
||||
@@ -4696,13 +4722,12 @@ export class LlEmitter {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of ${key}`);
|
||||
}
|
||||
const prefix = `sc_ldr_${this.liveDynRefAdapters.size}`;
|
||||
const adapter = streamTypedRefMaterializeAdapter(
|
||||
return streamTypedRefMaterializeAdapter(
|
||||
this,
|
||||
t,
|
||||
{ prefix, adapters: this.liveDynRefAdapters },
|
||||
`${prefix}_materialize`,
|
||||
);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
liveDynUnionRefAdapter(
|
||||
|
||||
@@ -235,7 +235,7 @@ export function emitSerializationExpr(host: LlvmEmitterContext, e: ExprOf<"jsonS
|
||||
// clamp/truncate rules); the interned re-indenter rewrites the
|
||||
// compact text with Node's gap algorithm. Compact temp stays
|
||||
// frame-owned; the pretty string is a fresh +1.
|
||||
const indent = (e as { indent?: string }).indent;
|
||||
const indent = e.indent;
|
||||
if (indent === undefined || indent === "") return compact;
|
||||
const rewriter = host.walkers.jsonIndentHelper();
|
||||
const t2 = B.tmp();
|
||||
|
||||
@@ -39,7 +39,8 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
}
|
||||
case "ffiCall": {
|
||||
// LIBRARY mode: every ffiCall is a profile-declared host-callback
|
||||
// channel (the library lane loads no native-FFI manifest). Fetch
|
||||
// channel (the library lane loads no native-FFI manifest). Wasm
|
||||
// calls named imports; native libraries fetch
|
||||
// the slot's registered pointer — scr_library_cb_require delivers
|
||||
// the channel's trap constant through the funnel (SC4025) when the
|
||||
// host never registered — then brackets the typed indirect call,
|
||||
@@ -50,7 +51,7 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
const libCb = host.mod.lib?.callbacks?.find((c) => c.name === e.import);
|
||||
if (libCb !== undefined) {
|
||||
const cbArgs = e.args.map((arg) => host.emitExpr(arg));
|
||||
const natTypes: string[] = ["ptr"];
|
||||
const natTypes: string[] = [];
|
||||
const natArgs: string[] = [];
|
||||
libCb.params.forEach((cls, i) => {
|
||||
const arg = cbArgs[i]!;
|
||||
@@ -99,10 +100,10 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
const len = B.tmp();
|
||||
const data = B.tmp();
|
||||
B.line(`${lenPtr} = getelementptr inbounds %ScrStr, ptr ${arg.name}, i64 0, i32 1`);
|
||||
B.line(`${len} = load i64, ptr ${lenPtr}`);
|
||||
B.line(`${data} = getelementptr inbounds i8, ptr ${arg.name}, i64 24`);
|
||||
natTypes.push("ptr", "i64");
|
||||
natArgs.push(`ptr ${data}`, `i64 ${len}`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}`);
|
||||
B.line(`${data} = getelementptr inbounds i8, ptr ${arg.name}, i64 ${host.abiOffset(24, 12)}`);
|
||||
natTypes.push("ptr", host.sizeType);
|
||||
natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`);
|
||||
break;
|
||||
}
|
||||
case "bytes": {
|
||||
@@ -110,26 +111,33 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
const len = B.tmp();
|
||||
const dataPtr = B.tmp();
|
||||
const data = B.tmp();
|
||||
B.line(`${lenPtr} = getelementptr inbounds i8, ptr ${arg.name}, i64 8`);
|
||||
B.line(`${len} = load i64, ptr ${lenPtr}`);
|
||||
B.line(`${dataPtr} = getelementptr inbounds i8, ptr ${arg.name}, i64 24`);
|
||||
B.line(`${lenPtr} = getelementptr inbounds i8, ptr ${arg.name}, i64 ${host.abiOffset(8, 4)}`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}`);
|
||||
B.line(`${dataPtr} = getelementptr inbounds i8, ptr ${arg.name}, i64 ${host.abiOffset(24, 12)}`);
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}`);
|
||||
natTypes.push("ptr", "i64");
|
||||
natArgs.push(`ptr ${data}`, `i64 ${len}`);
|
||||
natTypes.push("ptr", host.sizeType);
|
||||
natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
host.declare(`declare ptr @scr_library_cb_require(${host.sizeType}, ptr)`);
|
||||
host.declare(`declare ptr @scr_library_cb_ctx(${host.sizeType})`);
|
||||
host.declare(`declare void @scr_library_callback_begin()`);
|
||||
host.declare(`declare void @scr_library_callback_end()`);
|
||||
const fn = B.tmp();
|
||||
B.line(`${fn} = call ptr @scr_library_cb_require(${host.sizeType} ${libCb.slot}, ptr @sc_lib_cb_trap_${libCb.slot})`);
|
||||
const ctx = B.tmp();
|
||||
B.line(`${ctx} = call ptr @scr_library_cb_ctx(${host.sizeType} ${libCb.slot})`);
|
||||
const retTy = ffiNativeTypeLl(libCb.returns);
|
||||
const call = `call ${retTy} ${fn}(${[`ptr ${ctx}`, ...natArgs].join(", ")})`;
|
||||
let fn: string;
|
||||
if (host.wasi) {
|
||||
fn = `@sc_wasm_import_${libCb.slot}`;
|
||||
host.declare(`declare ${retTy} ${fn}(${natTypes.join(", ")}) "wasm-import-module"="scriptc" "wasm-import-name"="${libCb.name}"`);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_library_cb_require(${host.sizeType}, ptr)`);
|
||||
host.declare(`declare ptr @scr_library_cb_ctx(${host.sizeType})`);
|
||||
fn = B.tmp();
|
||||
B.line(`${fn} = call ptr @scr_library_cb_require(${host.sizeType} ${libCb.slot}, ptr @sc_lib_cb_trap_${libCb.slot})`);
|
||||
const ctx = B.tmp();
|
||||
B.line(`${ctx} = call ptr @scr_library_cb_ctx(${host.sizeType} ${libCb.slot})`);
|
||||
natArgs.unshift(`ptr ${ctx}`);
|
||||
}
|
||||
const call = `call ${retTy} ${fn}(${natArgs.join(", ")})`;
|
||||
if (libCb.returns === "void") {
|
||||
B.line(`call void @scr_library_callback_begin()`);
|
||||
B.line(call);
|
||||
|
||||
@@ -188,14 +188,14 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
|
||||
}
|
||||
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
|
||||
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
||||
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_key_delete(ptr, ptr, i1 zeroext)`);
|
||||
lines.push(
|
||||
` %bag = call ptr @scr_dyn_new_obj()`,
|
||||
` %bag_copy = call ptr @scr_dyn_copy_data_properties(ptr %bag, ptr %d)`,
|
||||
` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`,
|
||||
` call void @scr_dyn_release_v(ptr %bag_copy)`,
|
||||
` %bag_pending = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
|
||||
@@ -559,11 +559,9 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
// Reserve both names before generating field conversions: recursive
|
||||
// class/record conversions can request this adapter while it is in flight.
|
||||
if (isDynTypedRefType(t) || t.kind === "bytes" || t.kind === "array" || t.kind === "record") {
|
||||
adapter.commit = `@${snapshot}_commit`;
|
||||
}
|
||||
// Publish both symbols before their bodies: class-array and callback
|
||||
// converters can request this same adapter while emitting the commit.
|
||||
adapter.commit = `@${snapshot}_commit`;
|
||||
adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot);
|
||||
const B = new BlockBuilder();
|
||||
|
||||
@@ -602,13 +600,13 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
|
||||
}
|
||||
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
|
||||
host.declare(`declare ptr @scr_dyn_copy_data_properties(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
const slot = B.tmp(), bag = B.tmp(), copied = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`);
|
||||
B.line(`${bag} = load ptr, ptr ${slot}`);
|
||||
B.line(`${copied} = call ptr @scr_dyn_copy_data_properties(ptr ${out}, ptr ${bag})`);
|
||||
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
|
||||
}
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
@@ -700,6 +698,25 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
const len = B.tmp();
|
||||
B.line(`${len} = call double @scr_arr_len(ptr %p)`);
|
||||
B.countedLoop(len, (index) => {
|
||||
host.declare(`declare double @scr_arr_state(ptr, double)`);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
const state = B.tmp();
|
||||
const present = B.tmp();
|
||||
const valueLabel = B.newLabel("live.array.value");
|
||||
const absentLabel = B.newLabel("live.array.absent");
|
||||
const doneLabel = B.newLabel("live.array.done");
|
||||
B.line(`${state} = call double @scr_arr_state(ptr %p, double ${index})`);
|
||||
B.line(`${present} = fcmp oeq double ${state}, 1.0`); // SCR_ARR_VALUE
|
||||
B.condBr(present, valueLabel, absentLabel);
|
||||
B.startBlock(absentLabel);
|
||||
const undefinedValue = B.tmp();
|
||||
const retained = B.tmp();
|
||||
B.line(`${undefinedValue} = call ptr @scr_dyn_undefined()`);
|
||||
B.line(`${retained} = call ptr @scr_dyn_retain_v(ptr ${undefinedValue})`);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${retained})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(valueLabel);
|
||||
let value: string;
|
||||
if (elem.kind === "f64" || elem.kind === "bool") {
|
||||
const valueTy = elem.kind === "f64" ? "double" : "i1";
|
||||
@@ -718,6 +735,8 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
|
||||
if (isRefCounted(elem)) {
|
||||
B.line(`call void ${releaseSym(host.shapeHost, elem)}(ptr ${value})`);
|
||||
}
|
||||
B.br(doneLabel);
|
||||
B.startBlock(doneLabel);
|
||||
});
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MAY_THROW_LIB_FNS } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
import { LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -75,6 +76,39 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
|
||||
export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "dyn.typedRefIs") {
|
||||
// A literal brand cannot replace the receiver while being evaluated.
|
||||
// Generated class dispatch uses these probes repeatedly on one local.
|
||||
const value = e.args[1]!.kind === "strLit" ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
const key = host.emitExpr(e.args[1]!);
|
||||
// Expose the capsule tag to LLVM so class dispatch can skip the whole
|
||||
// chain of brand comparisons for ordinary checked-dynamic values.
|
||||
// Keep the runtime's null handling and evaluate both operands once.
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca i1`);
|
||||
B.line(`store i1 false, ptr ${slot}`);
|
||||
const present = B.tmp();
|
||||
const inspect = B.newLabel("brand.inspect");
|
||||
const capsule = B.newLabel("brand.capsule");
|
||||
const done = B.newLabel("brand.done");
|
||||
B.line(`${present} = icmp ne ptr ${value.name}, null`);
|
||||
B.condBr(present, inspect, done);
|
||||
B.startBlock(inspect);
|
||||
const kind = host.dynKind(value.name);
|
||||
const typed = B.tmp();
|
||||
B.line(`${typed} = icmp eq i32 ${kind}, ${DYN_KIND.TYPED_REF}`);
|
||||
B.condBr(typed, capsule, done);
|
||||
B.startBlock(capsule);
|
||||
host.declare("declare zeroext i1 @scr_dyn_typed_ref_is_key(ptr, ptr) memory(read)");
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = call zeroext i1 @scr_dyn_typed_ref_is_key(ptr ${value.name}, ptr ${key.name})`);
|
||||
B.line(`store i1 ${matches}, ptr ${slot}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = load i1, ptr ${slot}`);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
if (e.fn === "crypto.randomBytesCb" || e.fn === "crypto.pbkdf2Cb") {
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { join } from "node:path";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { EXTERNAL_OBJECT_ABI_STABILITY, RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js";
|
||||
import { executableLinkInputs } from "./link-plan-core.js";
|
||||
import type { RuntimePackArtifact, RuntimePackArtifacts, RuntimePackManifest } from "./runtime-pack-core.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export interface NativeLinkFeatures {
|
||||
dynamic: boolean;
|
||||
regex: boolean;
|
||||
copying: boolean;
|
||||
textDecoderLegacy: boolean;
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
netIsland: boolean;
|
||||
zlib: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
dynInvoke: boolean;
|
||||
dc: boolean;
|
||||
dynAsync: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
dgram: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
}
|
||||
|
||||
export interface NativeLinkInfo {
|
||||
schema: "scriptc.native-link-info.v1";
|
||||
format: 1;
|
||||
compiler_version: string;
|
||||
object_abi: {
|
||||
stability: "experimental";
|
||||
compatibility: "exact-runtime-version";
|
||||
};
|
||||
target: {
|
||||
name: NativeTargetSpec["name"];
|
||||
llvm_triple: NativeTargetSpec["llvmTriple"];
|
||||
architecture: NativeTargetSpec["architecture"];
|
||||
object_format: NativeTargetSpec["objectFormat"];
|
||||
minimum_os: NativeTargetSpec["minimumOs"];
|
||||
relocation_model: NativeTargetSpec["relocationModel"];
|
||||
};
|
||||
program: {
|
||||
object: string;
|
||||
entry_symbol: "main";
|
||||
undefined_runtime_symbol_prefix: "scr_";
|
||||
};
|
||||
runtime_abi: {
|
||||
version: typeof RUNTIME_ABI_VERSION;
|
||||
marker: typeof RUNTIME_ABI_MARKER;
|
||||
};
|
||||
runtime_pack: {
|
||||
kind: "precompiled";
|
||||
package: string;
|
||||
version: string;
|
||||
root: string;
|
||||
path_base: "runtime_pack.root";
|
||||
flavor: "release" | "dev";
|
||||
objects: RuntimePackArtifact[];
|
||||
archives: RuntimePackArtifact[];
|
||||
};
|
||||
ffi: {
|
||||
format: number | null;
|
||||
symbols: string[];
|
||||
libraries: string[];
|
||||
};
|
||||
link: {
|
||||
input_order: string[];
|
||||
driver_flags: string[];
|
||||
system_libraries: string[];
|
||||
frameworks: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/** Format the same external-link contract after either host verifies a pack. */
|
||||
export function formatNativeLinkInfo(options: {
|
||||
programObject: string;
|
||||
target: NativeTargetSpec;
|
||||
ffi: FfiProfile | null;
|
||||
}, compilerVersion: string, pack: {
|
||||
root: string;
|
||||
manifest: RuntimePackManifest;
|
||||
selected: RuntimePackArtifacts;
|
||||
flavor: "release" | "dev";
|
||||
}): NativeLinkInfo {
|
||||
const ffiLibraries = options.ffi?.libraries ?? [];
|
||||
const plan = executableLinkInputs({
|
||||
target: options.target, programObject: options.programObject,
|
||||
ffiLibraries, ffiSystemLibraries: options.ffi?.systemLibraries ?? [],
|
||||
ffiFrameworks: options.ffi?.frameworks ?? [], runtimeObjects: pack.selected.runtime.map((artifact) => join(pack.root, artifact.path)),
|
||||
runtimeArchives: pack.selected.archives.map((artifact) => join(pack.root, artifact.path)), runtimeSystemLibraries: pack.selected.systemLibraries,
|
||||
optimization: pack.flavor,
|
||||
});
|
||||
return {
|
||||
schema: "scriptc.native-link-info.v1",
|
||||
format: 1,
|
||||
compiler_version: compilerVersion,
|
||||
object_abi: {
|
||||
stability: EXTERNAL_OBJECT_ABI_STABILITY,
|
||||
compatibility: "exact-runtime-version",
|
||||
},
|
||||
target: {
|
||||
name: options.target.name,
|
||||
llvm_triple: options.target.llvmTriple,
|
||||
architecture: options.target.architecture,
|
||||
object_format: options.target.objectFormat,
|
||||
minimum_os: options.target.minimumOs,
|
||||
relocation_model: options.target.relocationModel,
|
||||
},
|
||||
program: {
|
||||
object: options.programObject,
|
||||
entry_symbol: "main",
|
||||
undefined_runtime_symbol_prefix: "scr_",
|
||||
},
|
||||
runtime_abi: {
|
||||
version: RUNTIME_ABI_VERSION,
|
||||
marker: RUNTIME_ABI_MARKER,
|
||||
},
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: pack.manifest.package,
|
||||
version: pack.manifest.version,
|
||||
root: pack.root,
|
||||
path_base: "runtime_pack.root",
|
||||
flavor: pack.flavor,
|
||||
objects: pack.selected.runtime,
|
||||
archives: pack.selected.archives,
|
||||
},
|
||||
ffi: {
|
||||
format: options.ffi?.ffiFormat ?? null,
|
||||
symbols: options.ffi?.functions.filter((fn) => !fn.callbackOperation).map((fn) => fn.symbol) ?? [],
|
||||
libraries: [...ffiLibraries],
|
||||
},
|
||||
link: {
|
||||
input_order: plan.inputs,
|
||||
driver_flags: plan.driverFlags,
|
||||
system_libraries: plan.systemLibraries,
|
||||
frameworks: [...(options.ffi?.frameworks ?? [])],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,92 +1,10 @@
|
||||
import { formatNativeLinkInfo, type NativeLinkInfo, type NativeLinkFeatures } from "./native-link-info-core.js";
|
||||
export type { NativeLinkInfo, NativeLinkFeatures } from "./native-link-info-core.js";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import { EXTERNAL_OBJECT_ABI_STABILITY, RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js";
|
||||
import { executableLinkInputs } from "./link-plan-core.js";
|
||||
import { loadRuntimePack } from "./runtime-pack.js";
|
||||
import type { RuntimePackArtifact } from "./runtime-pack-core.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export interface NativeLinkFeatures {
|
||||
dynamic: boolean;
|
||||
regex: boolean;
|
||||
copying: boolean;
|
||||
textDecoderLegacy: boolean;
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
netIsland: boolean;
|
||||
zlib: boolean;
|
||||
assert: boolean;
|
||||
inspect: boolean;
|
||||
dynInvoke: boolean;
|
||||
dc: boolean;
|
||||
dynAsync: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
dgram: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
}
|
||||
|
||||
export interface NativeLinkInfo {
|
||||
schema: "scriptc.native-link-info.v1";
|
||||
format: 1;
|
||||
compiler_version: string;
|
||||
object_abi: {
|
||||
stability: "experimental";
|
||||
compatibility: "exact-runtime-version";
|
||||
};
|
||||
target: {
|
||||
name: NativeTargetSpec["name"];
|
||||
llvm_triple: NativeTargetSpec["llvmTriple"];
|
||||
architecture: NativeTargetSpec["architecture"];
|
||||
object_format: NativeTargetSpec["objectFormat"];
|
||||
minimum_os: NativeTargetSpec["minimumOs"];
|
||||
relocation_model: NativeTargetSpec["relocationModel"];
|
||||
};
|
||||
program: {
|
||||
object: string;
|
||||
entry_symbol: "main";
|
||||
undefined_runtime_symbol_prefix: "scr_";
|
||||
};
|
||||
runtime_abi: {
|
||||
version: typeof RUNTIME_ABI_VERSION;
|
||||
marker: typeof RUNTIME_ABI_MARKER;
|
||||
};
|
||||
runtime_pack: {
|
||||
kind: "precompiled";
|
||||
package: string;
|
||||
version: string;
|
||||
root: string;
|
||||
path_base: "runtime_pack.root";
|
||||
flavor: "release" | "dev";
|
||||
objects: RuntimePackArtifact[];
|
||||
archives: RuntimePackArtifact[];
|
||||
};
|
||||
ffi: {
|
||||
format: number | null;
|
||||
symbols: string[];
|
||||
libraries: string[];
|
||||
};
|
||||
link: {
|
||||
input_order: string[];
|
||||
driver_flags: string[];
|
||||
system_libraries: string[];
|
||||
frameworks: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function createNativeLinkInfo(options: {
|
||||
programObject: string;
|
||||
target: NativeTargetSpec;
|
||||
@@ -100,59 +18,8 @@ export async function createNativeLinkInfo(options: {
|
||||
target: options.target, features: options.features,
|
||||
optimization: options.optimization ?? "release", ...(options.env === undefined ? {} : { env: options.env }),
|
||||
});
|
||||
const ffiLibraries = options.ffi?.libraries ?? [];
|
||||
const plan = executableLinkInputs({
|
||||
target: options.target, programObject: options.programObject,
|
||||
ffiLibraries, ffiSystemLibraries: options.ffi?.systemLibraries ?? [],
|
||||
ffiFrameworks: options.ffi?.frameworks ?? [], runtimeObjects: pack.runtimeObjects,
|
||||
runtimeArchives: pack.archives, runtimeSystemLibraries: pack.systemLibraries,
|
||||
optimization: pack.flavor,
|
||||
return formatNativeLinkInfo(options, compilerReleaseVersion(), {
|
||||
root: pack.root, manifest: pack.manifest, flavor: pack.flavor,
|
||||
selected: { features: pack.features, runtime: pack.selectedRuntimeArtifacts, archives: pack.selectedArchiveArtifacts, systemLibraries: pack.systemLibraries },
|
||||
});
|
||||
return {
|
||||
schema: "scriptc.native-link-info.v1",
|
||||
format: 1,
|
||||
compiler_version: compilerReleaseVersion(),
|
||||
object_abi: {
|
||||
stability: EXTERNAL_OBJECT_ABI_STABILITY,
|
||||
compatibility: "exact-runtime-version",
|
||||
},
|
||||
target: {
|
||||
name: options.target.name,
|
||||
llvm_triple: options.target.llvmTriple,
|
||||
architecture: options.target.architecture,
|
||||
object_format: options.target.objectFormat,
|
||||
minimum_os: options.target.minimumOs,
|
||||
relocation_model: options.target.relocationModel,
|
||||
},
|
||||
program: {
|
||||
object: options.programObject,
|
||||
entry_symbol: "main",
|
||||
undefined_runtime_symbol_prefix: "scr_",
|
||||
},
|
||||
runtime_abi: {
|
||||
version: RUNTIME_ABI_VERSION,
|
||||
marker: RUNTIME_ABI_MARKER,
|
||||
},
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: pack.manifest.package,
|
||||
version: pack.manifest.version,
|
||||
root: pack.root,
|
||||
path_base: "runtime_pack.root",
|
||||
flavor: pack.flavor,
|
||||
objects: pack.selectedRuntimeArtifacts,
|
||||
archives: pack.selectedArchiveArtifacts,
|
||||
},
|
||||
ffi: {
|
||||
format: options.ffi?.ffiFormat ?? null,
|
||||
symbols: options.ffi?.functions.filter((fn) => !fn.callbackOperation).map((fn) => fn.symbol) ?? [],
|
||||
libraries: [...ffiLibraries],
|
||||
},
|
||||
link: {
|
||||
input_order: plan.inputs,
|
||||
driver_flags: plan.driverFlags,
|
||||
system_libraries: plan.systemLibraries,
|
||||
frameworks: [...(options.ffi?.frameworks ?? [])],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { driverTraceCandidates, linkTraceCandidate } from "./link-trace.js";
|
||||
import { toolchainEnvironmentCachePolicy, toolchainEnvironmentFingerprint } from "./toolchain-environment.js";
|
||||
export { toolchainEnvironmentCachePolicy, toolchainEnvironmentFingerprint, type ToolchainEnvironmentCachePolicy } from "./toolchain-environment.js";
|
||||
import { IPHONEOS_MIN_VERSION, ANDROID_MIN_API, isIosTarget, isAndroidTarget, isMobileTarget, mobileLibraryTarget, mobileTargetRefusal, configuredTargetPlatform } from "./target-platform.js";
|
||||
export { IPHONEOS_MIN_VERSION, ANDROID_MIN_API, isIosTarget, isAndroidTarget, isMobileTarget, mobileLibraryTarget, mobileTargetRefusal, configuredTargetPlatform } from "./target-platform.js";
|
||||
import { InternalCompilerError } from "../errors.js";
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
@@ -123,114 +128,6 @@ export function executableSectionEliminationFlags(platform: string): {
|
||||
}
|
||||
}
|
||||
|
||||
/** Environment variables consumed by clang, its linker/subtools, or the
|
||||
* platform SDK selection. They are implicit command-line inputs: changing one
|
||||
* must never reuse an artifact produced under the old toolchain posture. */
|
||||
const TOOLCHAIN_ENV_KEYS = [
|
||||
"COMPILER_PATH",
|
||||
"GCC_EXEC_PREFIX",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OBJCPLUS_INCLUDE_PATH",
|
||||
"LIBRARY_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_RUN_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
||||
"SDKROOT",
|
||||
"DEVELOPER_DIR",
|
||||
"MACOSX_DEPLOYMENT_TARGET",
|
||||
"IPHONEOS_DEPLOYMENT_TARGET",
|
||||
"TVOS_DEPLOYMENT_TARGET",
|
||||
"WATCHOS_DEPLOYMENT_TARGET",
|
||||
"DRIVERKIT_DEPLOYMENT_TARGET",
|
||||
"XROS_DEPLOYMENT_TARGET",
|
||||
"CCC_OVERRIDE_OPTIONS",
|
||||
"CCC_ADD_ARGS",
|
||||
"CLANG_CONFIG_FILE_SYSTEM_DIR",
|
||||
"CLANG_CONFIG_FILE_USER_DIR",
|
||||
"CC",
|
||||
"CFLAGS",
|
||||
"CPPFLAGS",
|
||||
"LDFLAGS",
|
||||
"AR",
|
||||
"RANLIB",
|
||||
"CMAKE_GENERATOR",
|
||||
"CMAKE_TOOLCHAIN_FILE",
|
||||
"ZIG_LIB_DIR",
|
||||
"ZIG_LIBC",
|
||||
"SOURCE_DATE_EPOCH",
|
||||
"ZERO_AR_DATE",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
] as const;
|
||||
|
||||
/** Toolchain variables whose values name mutable files/directories consumed
|
||||
* while compiling a TU (or can inject arbitrary compiler options). Hashing the
|
||||
* value is insufficient: a header, SDK, config, compiler helper, or loaded
|
||||
* dylib can change in place while the spelling remains stable. In that posture
|
||||
* neither complete artifacts nor per-TU runtime objects are safe to reuse. */
|
||||
const MUTABLE_COMPILE_ENV_KEYS = [
|
||||
"COMPILER_PATH",
|
||||
"GCC_EXEC_PREFIX",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OBJCPLUS_INCLUDE_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
||||
"SDKROOT",
|
||||
"DEVELOPER_DIR",
|
||||
"CCC_OVERRIDE_OPTIONS",
|
||||
"CCC_ADD_ARGS",
|
||||
"CLANG_CONFIG_FILE_SYSTEM_DIR",
|
||||
"CLANG_CONFIG_FILE_USER_DIR",
|
||||
// `zig cc` resolves its bundled headers/runtime through ZIG_LIB_DIR and a
|
||||
// caller-selected native libc description through ZIG_LIBC. Both values name
|
||||
// mutable compiler inputs whose contents can change behind a stable path.
|
||||
"ZIG_LIB_DIR",
|
||||
"ZIG_LIBC",
|
||||
] as const;
|
||||
|
||||
/** These variables only redirect link-time inputs. Runtime objects remain
|
||||
* reusable, but a complete executable could otherwise retain a library that
|
||||
* was rebuilt in place behind the same search-path spelling. */
|
||||
const MUTABLE_LINK_ENV_KEYS = ["LIBRARY_PATH", "LD_RUN_PATH"] as const;
|
||||
|
||||
export interface ToolchainEnvironmentCachePolicy {
|
||||
completeArtifacts: boolean;
|
||||
runtimeObjects: boolean;
|
||||
}
|
||||
|
||||
export function toolchainEnvironmentCachePolicy(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ToolchainEnvironmentCachePolicy {
|
||||
const mutableCompileInput = MUTABLE_COMPILE_ENV_KEYS.some((name) => env[name] !== undefined);
|
||||
const mutableLinkInput = MUTABLE_LINK_ENV_KEYS.some((name) => env[name] !== undefined);
|
||||
return {
|
||||
completeArtifacts: !mutableCompileInput && !mutableLinkInput,
|
||||
runtimeObjects: !mutableCompileInput,
|
||||
};
|
||||
}
|
||||
|
||||
export function toolchainEnvironmentFingerprint(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const hash = createHash("sha256").update("toolchain-env-v1\0");
|
||||
for (const name of TOOLCHAIN_ENV_KEYS) {
|
||||
const value = env[name];
|
||||
hash.update(name).update(value === undefined ? "\0unset\0" : "\0set\0").update(value ?? "").update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** Inputs that can change which native tool/runtime implementation an
|
||||
* executable build selects before compileC has a chance to rediscover it.
|
||||
* The early whole-program cache keys this exact posture before restoring a
|
||||
@@ -645,59 +542,6 @@ export function isZigDriver(driver: Pick<CcDriver, "argv">): boolean {
|
||||
* embedder's side of the contract: Xcode links iOS archives against the
|
||||
* selected SDK, and Gradle/NDK builds link Android archives against the
|
||||
* API-26+ bionic stubs. */
|
||||
export const IPHONEOS_MIN_VERSION = "15.0";
|
||||
export const ANDROID_MIN_API = 26;
|
||||
|
||||
const MOBILE_LIBRARY_TARGETS = [
|
||||
"aarch64-apple-ios",
|
||||
"aarch64-apple-ios-simulator",
|
||||
"aarch64-linux-android",
|
||||
] as const;
|
||||
|
||||
export function isIosTarget(target: string | null): boolean {
|
||||
return target === "aarch64-apple-ios" || target === "aarch64-apple-ios-simulator";
|
||||
}
|
||||
|
||||
export function isAndroidTarget(target: string | null): boolean {
|
||||
return target === "aarch64-linux-android";
|
||||
}
|
||||
|
||||
export function isMobileTarget(target: string | null): boolean {
|
||||
return isIosTarget(target) || isAndroidTarget(target);
|
||||
}
|
||||
|
||||
/** The canonical mobile triple SCRIPTC_TARGET selects, or null when the
|
||||
* environment names none. Pure string inspection — safe to consult before
|
||||
* any toolchain discovery runs. */
|
||||
export function mobileLibraryTarget(env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
const target = env["SCRIPTC_TARGET"] ?? "";
|
||||
return isMobileTarget(target) ? target : null;
|
||||
}
|
||||
|
||||
/** The admission verdict for a mobile-family triple: null when the spelling
|
||||
* and host pairing are supported, otherwise the refusal text (the same text
|
||||
* resolveCc throws and compileLibrary reports as SC3002). Pure string/host
|
||||
* inspection — no discovery, no subprocess. */
|
||||
export function mobileTargetRefusal(
|
||||
target: string,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
): string | null {
|
||||
if (isIosTarget(target)) {
|
||||
return hostPlatform === "darwin"
|
||||
? null
|
||||
: `${target} library archives build on macOS hosts only (the Apple iOS SDK sysroot and Mach-O symbol localization live there); this host is ${hostPlatform}`;
|
||||
}
|
||||
if (isAndroidTarget(target)) return null;
|
||||
// A near-miss mobile spelling must refuse with the supported set named,
|
||||
// never reach zig with no sysroot wired (the compile would fail on the
|
||||
// first libc header) or produce an artifact for an unverified device
|
||||
// class.
|
||||
if (/(?:^|-)(?:ios|tvos|watchos|visionos|android)/.test(target)) {
|
||||
return `unsupported mobile target '${target}' (supported: ${MOBILE_LIBRARY_TARGETS.join(", ")})`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The Apple SDK root for one mobile platform, discovered through xcrun the
|
||||
* way Xcode's own build system selects it. Memoized per SDK name and
|
||||
* selection environment: production rediscovers per process, and the two
|
||||
@@ -906,34 +750,6 @@ function isMuslTarget(driver: Pick<CcDriver, "target">): boolean {
|
||||
* analyze(): the FRONTEND consults it too (path.sep / os.EOL literals and
|
||||
* the path-module binding follow the target — a win32 triple compiles
|
||||
* Node-on-Windows semantics, path.win32 backing the bare module). */
|
||||
export function configuredTargetPlatform(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
): string {
|
||||
const target = env["SCRIPTC_TARGET"] ?? "";
|
||||
if (target === "") return hostPlatform;
|
||||
if (target === "wasm32-wasi") return "wasi";
|
||||
if (target.includes("wasi")) {
|
||||
throw new Error(`unsupported WASI target '${target}' (supported: wasm32-wasi)`);
|
||||
}
|
||||
// iOS is a darwin-family target: Mach-O objects, ld64 localization,
|
||||
// POSIX path/EOL semantics. Android falls to the linux arm below —
|
||||
// bionic is a linux libc and its archives are ordinary ELF.
|
||||
if (isIosTarget(target)) return "darwin";
|
||||
if (isAndroidTarget(target)) return "linux";
|
||||
if (/(?:^|-)(?:ios|tvos|watchos|visionos|android)/.test(target)) {
|
||||
throw new Error(
|
||||
`unsupported mobile target '${target}' (supported: ${MOBILE_LIBRARY_TARGETS.join(", ")})`,
|
||||
);
|
||||
}
|
||||
if (target.includes("linux")) return "linux";
|
||||
if (target.includes("windows")) return "win32";
|
||||
if (target.includes("macos") || target.includes("darwin")) return "darwin";
|
||||
throw new Error(
|
||||
`unsupported target '${target}' (supported OS families: linux, windows, macos/darwin, wasm32-wasi)`,
|
||||
);
|
||||
}
|
||||
|
||||
export function targetPlatform(driver: CcDriver): string {
|
||||
if (driver.target === null) return process.platform;
|
||||
return configuredTargetPlatform({ SCRIPTC_TARGET: driver.target });
|
||||
@@ -1039,6 +855,7 @@ export interface LibArchiveOptions {
|
||||
* archive, byte-for-byte. */
|
||||
threadInstances?: boolean;
|
||||
/** IR-detected link gates (the compileC precedent, refusal-narrowed). */
|
||||
dynInvoke?: boolean;
|
||||
regex?: boolean;
|
||||
assert?: boolean;
|
||||
inspect?: boolean;
|
||||
@@ -1177,6 +994,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
...(opts.assert && opts.bigint ? ["scr_bigint_assert.c"] : []),
|
||||
...(opts.searchParams ? ["scr_url_params.c"] : []),
|
||||
...(opts.emitter ? ["scr_events_emitter.c", "scr_dyn_handle.c"] : []),
|
||||
...(opts.dynInvoke ? ["scr_dyn_invoke.c"] : []),
|
||||
...(opts.zlib ? ["scr_zlib.c"] : []),
|
||||
...(opts.copying ? ["scr_copying.c"] : []),
|
||||
];
|
||||
@@ -2978,29 +2796,6 @@ function implicitToolchainFingerprint(
|
||||
);
|
||||
}
|
||||
|
||||
function linkTraceCandidate(line: string): string[] {
|
||||
const trimmed = line.trim().replace(/^(?:LOAD|load)\s+/, "");
|
||||
if (trimmed === "") return [];
|
||||
const unquoted =
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
? trimmed.slice(1, -1)
|
||||
: trimmed;
|
||||
const candidates = [unquoted];
|
||||
const member = unquoted.lastIndexOf("(");
|
||||
if (member > 0 && unquoted.endsWith(")")) candidates.push(unquoted.slice(0, member));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function driverTraceCandidates(line: string): string[] {
|
||||
const candidates: string[] = [];
|
||||
for (const match of line.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) {
|
||||
const token = (match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["\\])/g, "$1");
|
||||
if (token !== "") candidates.push(token);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function existingDriverTracePaths(
|
||||
output: string,
|
||||
cwd: string,
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/** Native tool invocation without a JavaScript host or shell command construction. */
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import type { NativeHelperSpec, NativeTargetSpec } from "./targets.js";
|
||||
import { validateNativeCodegenVersion } from "./native-codegen-core.js";
|
||||
import { validateNativeCodegenVersion, type NativeCodegenOutputKind } from "./native-codegen-core.js";
|
||||
|
||||
export function runNativeTool(executable: string, args: string[]): string {
|
||||
const result = spawnSync(executable, args, { encoding: "utf8" });
|
||||
if (result.error) throw new Error(`${executable}: ${result.error.message}`);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${executable} failed (${result.signal ?? String(result.status)}): ${result.stderr.trim()}`);
|
||||
}
|
||||
return result.stdout;
|
||||
export function runNativeTool(executable: string, args: string[], cwd?: string, env?: NodeJS.ProcessEnv): string {
|
||||
return execFileSync(executable, args, { encoding: "utf8", stdio: "pipe", maxBuffer: 64 * 1024 * 1024,
|
||||
cwd: cwd ?? process.cwd(), env: env ?? process.env });
|
||||
}
|
||||
|
||||
export function requireNativeArtifact(path: string): void {
|
||||
@@ -28,6 +24,22 @@ export function emitNativeObject(options: {
|
||||
outputPath: string;
|
||||
sourcePath: string;
|
||||
optimization: "release" | "dev";
|
||||
outputKind?: NativeCodegenOutputKind;
|
||||
}): void {
|
||||
verifyNativeHelper(options);
|
||||
runNativeTool(options.executable, [
|
||||
"emit", "--input", options.inputPath, "--output", options.outputPath, "--filetype", options.outputKind ?? "obj",
|
||||
"--target", options.target.llvmTriple, "--opt-level", options.optimization === "dev" ? "0" : "2",
|
||||
"--relocation-model", options.target.relocationModel, "--diagnostic-format", "json",
|
||||
"--source-path", options.sourcePath,
|
||||
]);
|
||||
requireNativeArtifact(options.outputPath);
|
||||
}
|
||||
|
||||
/** Verify installed identity even when a caller can reuse a cached object. */
|
||||
export function verifyNativeHelper(options: {
|
||||
executable: string; packageRoot: string; compilerVersion: string;
|
||||
target: NativeTargetSpec; helper: NativeHelperSpec;
|
||||
}): void {
|
||||
const identity = JSON.parse(readFileSync(options.packageRoot + "/package.json", "utf8")) as { name: string; version: string };
|
||||
if (identity.name !== options.helper.packageName || identity.version !== options.compilerVersion) {
|
||||
@@ -35,11 +47,4 @@ export function emitNativeObject(options: {
|
||||
}
|
||||
const version = JSON.parse(runNativeTool(options.executable, ["version", "--format=json"])) as Record<string, unknown>;
|
||||
validateNativeCodegenVersion(version, options.target, options.helper, options.compilerVersion);
|
||||
runNativeTool(options.executable, [
|
||||
"emit", "--input", options.inputPath, "--output", options.outputPath, "--filetype", "obj",
|
||||
"--target", options.target.llvmTriple, "--opt-level", options.optimization === "dev" ? "0" : "2",
|
||||
"--relocation-model", options.target.relocationModel, "--diagnostic-format", "json",
|
||||
"--source-path", options.sourcePath,
|
||||
]);
|
||||
requireNativeArtifact(options.outputPath);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,10 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet<string>)
|
||||
for (let i = 0; i < sections.length; i++) sectionMap.push(sectionKept[i] === true ? next++ : -1);
|
||||
}
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
if (sectionKept[i] === true) sections[i]!.flags &= ~SHF_GROUP;
|
||||
if (sectionKept[i] === true) {
|
||||
const section = sections[i]!;
|
||||
section.flags = section.flags & ~SHF_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
// Demote and drop decisions per symbol. A symbol anchored to a dropped
|
||||
@@ -228,7 +231,9 @@ export function localizeElfObject(object: Uint8Array, keep: ReadonlySet<string>)
|
||||
}
|
||||
const symbolOrder = [...localOrder, ...globalOrder];
|
||||
const symbolMap: number[] = new Array<number>(symCount).fill(-1);
|
||||
symbolOrder.forEach((oldIndex, newIndex) => (symbolMap[oldIndex] = newIndex));
|
||||
symbolOrder.forEach((oldIndex, newIndex) => {
|
||||
symbolMap[oldIndex] = newIndex;
|
||||
});
|
||||
|
||||
const newSymtabData = new Uint8Array(symbolOrder.length * 24);
|
||||
const newSymtabView = new DataView(newSymtabData.buffer);
|
||||
@@ -412,7 +417,8 @@ function parseCoff(object: Uint8Array, label: string): CoffObject {
|
||||
const sectionName = (raw: Uint8Array): string => {
|
||||
if (raw[0] === 0x2f /* '/' */) {
|
||||
const spelled = textDecoder.decode(raw.subarray(1)).replace(/\0+$/, "").trim();
|
||||
const offset = Number.parseInt(spelled, 10);
|
||||
const decimal = /^\d+/.exec(spelled);
|
||||
const offset = decimal === null ? NaN : Number(decimal[0]);
|
||||
if (!Number.isFinite(offset)) fail(`${label}: malformed long section name`);
|
||||
return readCString(strtab, offset);
|
||||
}
|
||||
@@ -563,7 +569,7 @@ export function mergeAndLocalizeCoffObjects(
|
||||
];
|
||||
for (const object of objects) {
|
||||
if (object.machine !== IMAGE_FILE_MACHINE_AMD64) {
|
||||
fail(`${object.label}: unsupported COFF machine 0x${object.machine.toString(16)}`);
|
||||
fail(`${object.label}: unsupported COFF machine ${object.machine}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,7 +607,7 @@ export function mergeAndLocalizeCoffObjects(
|
||||
for (const sym of object.symbols) {
|
||||
if (!coffIsUndefined(sym)) continue;
|
||||
if (selectedDefinitions.has(sym.name)) continue;
|
||||
for (const candidate of definers.get(sym.name) ?? []) {
|
||||
for (const candidate of definers.get(sym.name) ?? new Array<number>()) {
|
||||
if (included[candidate] !== true) {
|
||||
included[candidate] = true;
|
||||
addDefinitions(objects[candidate]!);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
import {
|
||||
RuntimePackError, parseRuntimePackManifest, selectRuntimePackArtifacts, validateRuntimePackIdentity,
|
||||
type RuntimePackArtifact,
|
||||
type RuntimePackArtifact, type RuntimePackArtifacts, type RuntimePackManifest, type RuntimePackMode,
|
||||
} from "./runtime-pack-core.js";
|
||||
|
||||
export interface NativeRuntimePack {
|
||||
@@ -17,6 +17,28 @@ export interface NativeRuntimePack {
|
||||
systemLibraries: string[];
|
||||
}
|
||||
|
||||
export interface NativeRuntimeSelection {
|
||||
root: string;
|
||||
manifest: RuntimePackManifest;
|
||||
packageText: string;
|
||||
manifestText: string;
|
||||
selected: RuntimePackArtifacts;
|
||||
flavor: "release" | "dev";
|
||||
}
|
||||
|
||||
export function selectNativeRuntimePack(
|
||||
root: string, target: NativeTargetSpec, compilerVersion: string,
|
||||
features: NativeLinkFeatures, flavor: "release" | "dev", mode: RuntimePackMode = "executable",
|
||||
): NativeRuntimeSelection {
|
||||
const packageText = readFileSync(join(root, "package.json"), "utf8");
|
||||
const manifestText = readFileSync(join(root, "runtime-pack.json"), "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
const manifest = parseRuntimePackManifest(JSON.parse(manifestText));
|
||||
validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, features, flavor, process.env, mode);
|
||||
return { root, manifest, packageText, manifestText, selected, flavor };
|
||||
}
|
||||
|
||||
function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
@@ -48,15 +70,15 @@ export function stageNativeRuntimePack(
|
||||
compilerVersion: string,
|
||||
features: NativeLinkFeatures,
|
||||
flavor: "release" | "dev",
|
||||
mode: RuntimePackMode = "executable",
|
||||
): NativeRuntimePack {
|
||||
return stageNativeRuntimeSelection(selectNativeRuntimePack(root, target, compilerVersion, features, flavor, mode), stageRoot);
|
||||
}
|
||||
|
||||
export function stageNativeRuntimeSelection(selection: NativeRuntimeSelection, stageRoot: string): NativeRuntimePack {
|
||||
const { root, manifest, selected, packageText, manifestText } = selection;
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
const manifest = parseRuntimePackManifest(JSON.parse(manifestText));
|
||||
validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, features, flavor);
|
||||
const runtimeObjects = selected.runtime.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
for (const license of manifest.licenses) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { everyModuleNode } from "../ir/traverse.js";
|
||||
import type { LlvmUnsupportedError } from "./llvm/emitter.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import { moduleUsesFetch, moduleEmbedsBuiltin, type IrModule, type SrcLoc } from "../ir/ir.js";
|
||||
|
||||
/** The LLVM backend's tier refusal as a diagnostic. SC3xxx = backend
|
||||
* coverage (the program is fine — this backend doesn't compile it yet);
|
||||
* the parenthesized kind tag is machine-readable for the differential
|
||||
* harness's histogram. */
|
||||
export function llvmRefusalDiag(err: LlvmUnsupportedError, entryPath: string): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC3001",
|
||||
message: err.message,
|
||||
loc: err.loc ?? { file: entryPath, start: 0, end: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/** A valid program surface that the selected execution target cannot host.
|
||||
* SC3xxx stays the backend/target-coverage family: source semantics are
|
||||
* valid, but this target deliberately refuses them instead of emitting a
|
||||
* binary that traps later. */
|
||||
export function targetRefusalDiag(target: string, surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC3002",
|
||||
message: `${target} target does not support ${surface}`,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** APIs that require host capabilities absent from portable WASI Preview 1.
|
||||
* These are target diagnostics, not backend-tier gaps: the same language IR
|
||||
* (including async, generators, and the dynamic island) is otherwise valid.
|
||||
* Keep the fine-grained walk first so diagnostics point at the API use; the
|
||||
* embedded-module checks are the entry-anchored safety net for island code. */
|
||||
export function moduleWasiUnavailableSurface(mod: IrModule): { surface: string; loc: SrcLoc } | null {
|
||||
const entryLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
|
||||
const prefixes: readonly (readonly [string, string])[] = [
|
||||
["cp.", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["child.", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["spawnRes.", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["net.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["http.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["https.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["http2.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["h2.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["dgram.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["dns.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["tls.", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["fetch.", "network-backed fetch (WASI Preview 1 has no socket API)"],
|
||||
["fs.watch", "filesystem watching (WASI Preview 1 has no notification API)"],
|
||||
["watcher.", "filesystem watching (WASI Preview 1 has no notification API)"],
|
||||
];
|
||||
const kinds: ReadonlyMap<string, string> = new Map([
|
||||
["child", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["spawnRes", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["childStream", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["childWriter", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["netServer", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["netSocket", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["http2Session", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["http2Stream", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["dgramSocket", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["fsWatcher", "filesystem watching (WASI Preview 1 has no notification API)"],
|
||||
["httpReq", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["httpRes", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["httpClientReq", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["secureCtx", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
]);
|
||||
let found: { surface: string; loc: SrcLoc } | null = null;
|
||||
everyModuleNode(mod, {
|
||||
type: (node, loc) => {
|
||||
const surface = kinds.get(node.kind);
|
||||
if (surface === undefined) return true;
|
||||
found = { surface, loc };
|
||||
return false;
|
||||
},
|
||||
stmt: () => true,
|
||||
expr: (node) => {
|
||||
if (node.kind !== "libCall") return true;
|
||||
const loc = node.loc;
|
||||
if (node.fn === "process.kill" || node.fn === "process.killNum" ||
|
||||
node.fn === "process.onSignal" || node.fn === "process.offSignal") {
|
||||
found = { surface: "OS signals (WASI Preview 1 has no signal API)", loc };
|
||||
return false;
|
||||
}
|
||||
if (node.fn === "os.networkInterfaces") {
|
||||
found = { surface: "network-interface enumeration (WASI Preview 1 has no interface API)", loc };
|
||||
return false;
|
||||
}
|
||||
for (const [prefix, surface] of prefixes) {
|
||||
if (node.fn.startsWith(prefix)) {
|
||||
found = { surface, loc };
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (found !== null) return found;
|
||||
|
||||
if (moduleUsesFetch(mod)) {
|
||||
return { surface: "network-backed fetch (WASI Preview 1 has no socket API)", loc: entryLoc };
|
||||
}
|
||||
for (const builtin of ["node:http", "node:https", "node:net", "node:tls"]) {
|
||||
if (moduleEmbedsBuiltin(mod, builtin)) {
|
||||
return { surface: `${builtin} networking (WASI Preview 1 has no socket API)`, loc: entryLoc };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export const IPHONEOS_MIN_VERSION = "15.0";
|
||||
export const ANDROID_MIN_API = 26;
|
||||
|
||||
const MOBILE_LIBRARY_TARGETS = [
|
||||
"aarch64-apple-ios",
|
||||
"aarch64-apple-ios-simulator",
|
||||
"aarch64-linux-android",
|
||||
] as const;
|
||||
|
||||
export function isIosTarget(target: string | null): boolean {
|
||||
return target === "aarch64-apple-ios" || target === "aarch64-apple-ios-simulator";
|
||||
}
|
||||
|
||||
export function isAndroidTarget(target: string | null): boolean {
|
||||
return target === "aarch64-linux-android";
|
||||
}
|
||||
|
||||
export function isMobileTarget(target: string | null): boolean {
|
||||
return isIosTarget(target) || isAndroidTarget(target);
|
||||
}
|
||||
|
||||
/** The canonical mobile triple SCRIPTC_TARGET selects, or null when the
|
||||
* environment names none. Pure string inspection — safe to consult before
|
||||
* any toolchain discovery runs. */
|
||||
export function mobileLibraryTarget(env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
const target = env["SCRIPTC_TARGET"] ?? "";
|
||||
return isMobileTarget(target) ? target : null;
|
||||
}
|
||||
|
||||
/** The admission verdict for a mobile-family triple: null when the spelling
|
||||
* and host pairing are supported, otherwise the refusal text (the same text
|
||||
* resolveCc throws and compileLibrary reports as SC3002). Pure string/host
|
||||
* inspection — no discovery, no subprocess. */
|
||||
export function mobileTargetRefusal(
|
||||
target: string,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
): string | null {
|
||||
if (isIosTarget(target)) {
|
||||
return hostPlatform === "darwin"
|
||||
? null
|
||||
: `${target} library archives build on macOS hosts only (the Apple iOS SDK sysroot and Mach-O symbol localization live there); this host is ${hostPlatform}`;
|
||||
}
|
||||
if (isAndroidTarget(target)) return null;
|
||||
// A near-miss mobile spelling must refuse with the supported set named,
|
||||
// never reach zig with no sysroot wired (the compile would fail on the
|
||||
// first libc header) or produce an artifact for an unverified device
|
||||
// class.
|
||||
if (/(?:^|-)(?:ios|tvos|watchos|visionos|android)/.test(target)) {
|
||||
return `unsupported mobile target '${target}' (supported: ${MOBILE_LIBRARY_TARGETS.join(", ")})`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function configuredTargetPlatform(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
): string {
|
||||
const target = env["SCRIPTC_TARGET"] ?? "";
|
||||
if (target === "") return hostPlatform;
|
||||
if (target === "wasm32-wasi") return "wasi";
|
||||
if (target.includes("wasi")) {
|
||||
throw new Error(`unsupported WASI target '${target}' (supported: wasm32-wasi)`);
|
||||
}
|
||||
// iOS is a darwin-family target: Mach-O objects, ld64 localization,
|
||||
// POSIX path/EOL semantics. Android falls to the linux arm below —
|
||||
// bionic is a linux libc and its archives are ordinary ELF.
|
||||
if (isIosTarget(target)) return "darwin";
|
||||
if (isAndroidTarget(target)) return "linux";
|
||||
if (/(?:^|-)(?:ios|tvos|watchos|visionos|android)/.test(target)) {
|
||||
throw new Error(
|
||||
`unsupported mobile target '${target}' (supported: ${MOBILE_LIBRARY_TARGETS.join(", ")})`,
|
||||
);
|
||||
}
|
||||
if (target.includes("linux")) return "linux";
|
||||
if (target.includes("windows")) return "win32";
|
||||
if (target.includes("macos") || target.includes("darwin")) return "darwin";
|
||||
throw new Error(
|
||||
`unsupported target '${target}' (supported OS families: linux, windows, macos/darwin, wasm32-wasi)`,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
IOS_ARM64_TARGET,
|
||||
IOS_SIMULATOR_ARM64_TARGET,
|
||||
@@ -19,7 +19,46 @@ import {
|
||||
windowsSubsystemLinkerArgs,
|
||||
} from "./targets.js";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("native code-generation targets", () => {
|
||||
test("non-Linux and unsupported hosts never collect a process report", async () => {
|
||||
vi.resetModules();
|
||||
const targets = await import("./targets.js");
|
||||
const report = vi.spyOn(process.report, "getReport").mockImplementation(() => {
|
||||
throw new Error("unexpected host libc probe");
|
||||
});
|
||||
for (const [platform, arch, release] of [
|
||||
["darwin", "arm64", "24.0.0"], ["darwin", "x64", "24.0.0"], ["win32", "x64", "10.0.0"],
|
||||
] as const) {
|
||||
expect(targets.nativeCodegenTarget({}, platform, arch, release)).not.toBeNull();
|
||||
expect(targets.nativeCodegenTargetRefusal({}, platform, arch, release)).toBeNull();
|
||||
expect(targets.nativeHelperForTarget(LINUX_X64_GNU_TARGET, platform, arch)).not.toBeNull();
|
||||
}
|
||||
expect(targets.nativeCodegenTarget({}, "linux", "ia32", "6.8.0")).toBeNull();
|
||||
expect(targets.nativeHelperForTarget(LINUX_X64_GNU_TARGET, "linux", "ia32")).toBeNull();
|
||||
expect(report).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each(["gnu", "musl"] as const)("probes the %s host libc once across target and helper selection", async (libc) => {
|
||||
vi.resetModules();
|
||||
const targets = await import("./targets.js");
|
||||
const report = vi.spyOn(process.report, "getReport").mockReturnValue({
|
||||
header: libc === "gnu" ? { glibcVersionRuntime: "2.36" } : {},
|
||||
} as ReturnType<typeof process.report.getReport>);
|
||||
const target = libc === "gnu" ? LINUX_X64_GNU_TARGET : LINUX_X64_MUSL_TARGET;
|
||||
expect(targets.nativeCodegenTarget({}, "linux", "x64", "6.8.0")).toEqual(target);
|
||||
expect(targets.nativeCodegenTargetRefusal({}, "linux", "x64", "6.8.0")).toBeNull();
|
||||
expect(targets.nativeHelperForTarget(target, "linux", "x64")?.packageName).toBe(`@scriptc/llvm-linux-x64-${libc}`);
|
||||
expect(targets.nativeCodegenTarget({ SCRIPTC_TARGET: "wasm32-wasi" }, "linux", "x64", "6.8.0")).toEqual(WASM32_WASI_TARGET);
|
||||
expect(report).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Explicit host descriptions remain independent of the process memo.
|
||||
expect(targets.nativeCodegenTarget({}, "linux", "x64", "6.8.0", "gnu")).toEqual(LINUX_X64_GNU_TARGET);
|
||||
expect(targets.nativeCodegenTarget({}, "linux", "x64", "6.8.0", "musl")).toEqual(LINUX_X64_MUSL_TARGET);
|
||||
expect(report).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("matches Clang's narrow integer ABI independently of the build host", () => {
|
||||
for (const target of [MACOS_ARM64_TARGET, MACOS_X64_TARGET, LINUX_X64_GNU_TARGET, LINUX_X64_MUSL_TARGET, WASM32_WASI_TARGET]) {
|
||||
expect(ffiExtendsNarrowIntegers(target.llvmTriple, "win32", "x64")).toBe(true);
|
||||
|
||||
@@ -294,7 +294,7 @@ export const WASM32_WASI_TARGET: NativeTargetSpec = {
|
||||
linkerTargetTriple: "wasm32-wasi",
|
||||
runtimeSystemLibraries: ["wasi-emulated-signal", "wasi-emulated-process-clocks"],
|
||||
runtimeCompileDefines: ["_GNU_SOURCE", "_WASI_EMULATED_SIGNAL", "_WASI_EMULATED_PROCESS_CLOCKS"],
|
||||
supports: { asm: true, obj: true, exe: true, library: false },
|
||||
supports: { asm: true, obj: true, exe: true, library: true },
|
||||
helperPackage: DARWIN_ARM64_HELPER.packageName, helper: DARWIN_ARM64_HELPER,
|
||||
llvmBackend: "WebAssembly",
|
||||
hostHelpers: {
|
||||
@@ -340,24 +340,29 @@ export const NATIVE_TARGETS = [
|
||||
IOS_ARM64_TARGET, IOS_SIMULATOR_ARM64_TARGET, ANDROID_ARM64_TARGET,
|
||||
] as const;
|
||||
|
||||
let linuxLibcMemo: LinuxLibc | undefined;
|
||||
|
||||
function detectedLinuxLibc(): LinuxLibc {
|
||||
if (linuxLibcMemo !== undefined) return linuxLibcMemo;
|
||||
// Node exposes glibc's runtime version without any external command or
|
||||
// filesystem probe. Its absence on Linux is the portable musl signal used
|
||||
// by npm's own optional-dependency selection conventions.
|
||||
// by npm's own optional-dependency selection conventions. The host libc
|
||||
// cannot change within this process, and generating a full report is costly.
|
||||
const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: unknown } } | undefined;
|
||||
const header = report?.header;
|
||||
return typeof header?.glibcVersionRuntime === "string" ? "gnu" : "musl";
|
||||
linuxLibcMemo = typeof header?.glibcVersionRuntime === "string" ? "gnu" : "musl";
|
||||
return linuxLibcMemo;
|
||||
}
|
||||
|
||||
function helperHost(
|
||||
platform: NodeJS.Platform,
|
||||
arch: string,
|
||||
linuxLibc: LinuxLibc = detectedLinuxLibc(),
|
||||
linuxLibc?: LinuxLibc,
|
||||
): NativeHelperHost | null {
|
||||
if (platform === "darwin" && arch === "arm64") return "darwin-arm64";
|
||||
if (platform === "darwin" && arch === "x64") return "darwin-x64";
|
||||
if (platform === "linux" && arch === "x64") return `linux-x64-${linuxLibc}`;
|
||||
if (platform === "linux" && arch === "arm64") return `linux-arm64-${linuxLibc}`;
|
||||
if (platform === "linux" && arch === "x64") return `linux-x64-${linuxLibc ?? detectedLinuxLibc()}`;
|
||||
if (platform === "linux" && arch === "arm64") return `linux-arm64-${linuxLibc ?? detectedLinuxLibc()}`;
|
||||
if (platform === "win32" && arch === "x64") return "win32-x64";
|
||||
return null;
|
||||
}
|
||||
@@ -385,19 +390,18 @@ function nativeHostTarget(
|
||||
hostPlatform: NodeJS.Platform,
|
||||
hostArch: string,
|
||||
hostRelease: string,
|
||||
linuxLibc: LinuxLibc,
|
||||
linuxLibc?: LinuxLibc,
|
||||
): NativeTargetSpec | null {
|
||||
if (hostPlatform === "darwin") {
|
||||
const major = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10);
|
||||
if (!Number.isFinite(major) || major < 24) return null;
|
||||
return hostArch === "arm64" ? MACOS_ARM64_TARGET : hostArch === "x64" ? MACOS_X64_TARGET : null;
|
||||
}
|
||||
if (hostPlatform === "linux") {
|
||||
if (hostPlatform === "linux" && (hostArch === "x64" || hostArch === "arm64")) {
|
||||
const libc = linuxLibc ?? detectedLinuxLibc();
|
||||
return hostArch === "x64"
|
||||
? linuxLibc === "musl" ? LINUX_X64_MUSL_TARGET : LINUX_X64_GNU_TARGET
|
||||
: hostArch === "arm64"
|
||||
? linuxLibc === "musl" ? LINUX_ARM64_MUSL_TARGET : LINUX_ARM64_GNU_TARGET
|
||||
: null;
|
||||
? libc === "musl" ? LINUX_X64_MUSL_TARGET : LINUX_X64_GNU_TARGET
|
||||
: libc === "musl" ? LINUX_ARM64_MUSL_TARGET : LINUX_ARM64_GNU_TARGET;
|
||||
}
|
||||
return hostPlatform === "win32" && hostArch === "x64" ? WINDOWS_X64_MSVC_TARGET : null;
|
||||
}
|
||||
@@ -435,20 +439,12 @@ function requestedTarget(
|
||||
}
|
||||
}
|
||||
|
||||
/** Select only a fully described scriptc target. LLVM accepting an arbitrary
|
||||
* triple is never evidence of its object ABI, runtime pack, link, or run. */
|
||||
export function nativeCodegenTarget(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
hostArch: string = process.arch,
|
||||
hostRelease: string = release(),
|
||||
linuxLibc: LinuxLibc = detectedLinuxLibc(),
|
||||
/** Native distributions already know their host ABI, including Linux libc. */
|
||||
export function selectNativeTarget(
|
||||
raw: string, host: NativeTargetSpec, hostPlatform: string = process.platform,
|
||||
): NativeTargetSpec | null {
|
||||
const host = nativeHostTarget(hostPlatform, hostArch, hostRelease, linuxLibc);
|
||||
const target = requestedTarget(env["SCRIPTC_TARGET"] ?? "", host, hostPlatform);
|
||||
if (host === null || target === null || nativeHelperForTarget(target, hostPlatform, hostArch, linuxLibc) === null) return null;
|
||||
// Cross ELF links use Zig's target libc. A native clang driver cannot
|
||||
// infer or provide another architecture's CRT and sysroot.
|
||||
const target = requestedTarget(raw, host, hostPlatform as NodeJS.Platform);
|
||||
if (target === null) return null;
|
||||
if (target.platform === "linux" && target.name !== host.name && target.name !== "android-arm64") {
|
||||
return {
|
||||
...target, defaultLinker: "zig", defaultLinkerArgs: ["cc"],
|
||||
@@ -461,12 +457,27 @@ export function nativeCodegenTarget(
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Select only a fully described scriptc target. LLVM accepting an arbitrary
|
||||
* triple is never evidence of its object ABI, runtime pack, link, or run. */
|
||||
export function nativeCodegenTarget(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
hostArch: string = process.arch,
|
||||
hostRelease: string = release(),
|
||||
linuxLibc?: LinuxLibc,
|
||||
): NativeTargetSpec | null {
|
||||
const host = nativeHostTarget(hostPlatform, hostArch, hostRelease, linuxLibc);
|
||||
const target = host === null ? null : selectNativeTarget(env["SCRIPTC_TARGET"] ?? "", host, hostPlatform);
|
||||
if (host === null || target === null || nativeHelperForTarget(target, hostPlatform, hostArch, linuxLibc) === null) return null;
|
||||
return target;
|
||||
}
|
||||
|
||||
export function nativeCodegenTargetRefusal(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
hostPlatform: NodeJS.Platform = process.platform,
|
||||
hostArch: string = process.arch,
|
||||
hostRelease: string = release(),
|
||||
linuxLibc: LinuxLibc = detectedLinuxLibc(),
|
||||
linuxLibc?: LinuxLibc,
|
||||
): string | null {
|
||||
if (nativeCodegenTarget(env, hostPlatform, hostArch, hostRelease, linuxLibc) !== null) return null;
|
||||
const requested = env["SCRIPTC_TARGET"] ?? "";
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
/** Environment variables consumed by clang, its linker/subtools, or the
|
||||
* platform SDK selection. They are implicit command-line inputs: changing one
|
||||
* must never reuse an artifact produced under the old toolchain posture. */
|
||||
const TOOLCHAIN_ENV_KEYS: readonly string[] = [
|
||||
"COMPILER_PATH",
|
||||
"GCC_EXEC_PREFIX",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OBJCPLUS_INCLUDE_PATH",
|
||||
"LIBRARY_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_RUN_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
||||
"SDKROOT",
|
||||
"DEVELOPER_DIR",
|
||||
"MACOSX_DEPLOYMENT_TARGET",
|
||||
"IPHONEOS_DEPLOYMENT_TARGET",
|
||||
"TVOS_DEPLOYMENT_TARGET",
|
||||
"WATCHOS_DEPLOYMENT_TARGET",
|
||||
"DRIVERKIT_DEPLOYMENT_TARGET",
|
||||
"XROS_DEPLOYMENT_TARGET",
|
||||
"CCC_OVERRIDE_OPTIONS",
|
||||
"CCC_ADD_ARGS",
|
||||
"CLANG_CONFIG_FILE_SYSTEM_DIR",
|
||||
"CLANG_CONFIG_FILE_USER_DIR",
|
||||
"CC",
|
||||
"CFLAGS",
|
||||
"CPPFLAGS",
|
||||
"LDFLAGS",
|
||||
"AR",
|
||||
"RANLIB",
|
||||
"CMAKE_GENERATOR",
|
||||
"CMAKE_TOOLCHAIN_FILE",
|
||||
"ZIG_LIB_DIR",
|
||||
"ZIG_LIBC",
|
||||
"SOURCE_DATE_EPOCH",
|
||||
"ZERO_AR_DATE",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
];
|
||||
|
||||
/** Toolchain variables whose values name mutable files/directories consumed
|
||||
* while compiling a TU (or can inject arbitrary compiler options). Hashing the
|
||||
* value is insufficient: a header, SDK, config, compiler helper, or loaded
|
||||
* dylib can change in place while the spelling remains stable. In that posture
|
||||
* neither complete artifacts nor per-TU runtime objects are safe to reuse. */
|
||||
const MUTABLE_COMPILE_ENV_KEYS: readonly string[] = [
|
||||
"COMPILER_PATH",
|
||||
"GCC_EXEC_PREFIX",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OBJCPLUS_INCLUDE_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
||||
"SDKROOT",
|
||||
"DEVELOPER_DIR",
|
||||
"CCC_OVERRIDE_OPTIONS",
|
||||
"CCC_ADD_ARGS",
|
||||
"CLANG_CONFIG_FILE_SYSTEM_DIR",
|
||||
"CLANG_CONFIG_FILE_USER_DIR",
|
||||
// `zig cc` resolves its bundled headers/runtime through ZIG_LIB_DIR and a
|
||||
// caller-selected native libc description through ZIG_LIBC. Both values name
|
||||
// mutable compiler inputs whose contents can change behind a stable path.
|
||||
"ZIG_LIB_DIR",
|
||||
"ZIG_LIBC",
|
||||
];
|
||||
|
||||
/** These variables only redirect link-time inputs. Runtime objects remain
|
||||
* reusable, but a complete executable could otherwise retain a library that
|
||||
* was rebuilt in place behind the same search-path spelling. */
|
||||
const MUTABLE_LINK_ENV_KEYS: readonly string[] = ["LIBRARY_PATH", "LD_RUN_PATH"];
|
||||
|
||||
export interface ToolchainEnvironmentCachePolicy {
|
||||
completeArtifacts: boolean;
|
||||
runtimeObjects: boolean;
|
||||
}
|
||||
|
||||
export function toolchainEnvironmentCachePolicy(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ToolchainEnvironmentCachePolicy {
|
||||
const mutableCompileInput = MUTABLE_COMPILE_ENV_KEYS.some((name) => env[name] !== undefined);
|
||||
const mutableLinkInput = MUTABLE_LINK_ENV_KEYS.some((name) => env[name] !== undefined);
|
||||
return {
|
||||
completeArtifacts: !mutableCompileInput && !mutableLinkInput,
|
||||
runtimeObjects: !mutableCompileInput,
|
||||
};
|
||||
}
|
||||
|
||||
export function toolchainEnvironmentFingerprint(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const hash = createHash("sha256").update("toolchain-env-v1\0");
|
||||
for (const name of TOOLCHAIN_ENV_KEYS) {
|
||||
const value = env[name];
|
||||
const text: string = value ?? "";
|
||||
hash.update(name).update(value === undefined ? "\0unset\0" : "\0set\0").update(text).update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
@@ -9,15 +9,8 @@ import type { CcDriver } from "./native-toolchain.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** The pinned QuickJS, mbedTLS, and zlib inputs used by native recipes. */
|
||||
export const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7";
|
||||
export const MBEDTLS_VERSION = "3.6.7";
|
||||
export const ZLIB_VERSION = "1.3.1";
|
||||
/** Exact translation-unit membership shared by cache builds and external
|
||||
* source-pack recipes. */
|
||||
export const QJS_ENGINE_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"] as const;
|
||||
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
|
||||
export const ZLIB_SOURCES = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"] as const;
|
||||
import { QJS_COMMIT, MBEDTLS_VERSION, ZLIB_VERSION, QJS_ENGINE_SOURCES, LRE_SOURCES, ZLIB_SOURCES } from "./vendor-inputs.js";
|
||||
export { QJS_COMMIT, MBEDTLS_VERSION, ZLIB_VERSION, QJS_ENGINE_SOURCES, LRE_SOURCES, ZLIB_SOURCES } from "./vendor-inputs.js";
|
||||
|
||||
export interface VendorArchiveContext {
|
||||
runtimeSrcDir(): string;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** The pinned QuickJS, mbedTLS, and zlib inputs used by native recipes. */
|
||||
export const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7";
|
||||
export const MBEDTLS_VERSION = "3.6.7";
|
||||
export const ZLIB_VERSION = "1.3.1";
|
||||
/** Exact translation-unit membership shared by cache builds and external
|
||||
* source-pack recipes. */
|
||||
export const QJS_ENGINE_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"] as const;
|
||||
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
|
||||
export const ZLIB_SOURCES = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"] as const;
|
||||
@@ -0,0 +1,325 @@
|
||||
import { rmSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { isExactExternalTypeSpecifier } from "../frontend/program.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import { renderCoverage } from "../coverage/report.js";
|
||||
import { setProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { CliHost, NativeCacheWarmProfile } from "./host.js";
|
||||
import { resolveOutputOptions } from "./output-options.js";
|
||||
import { selectOutputPaths } from "./paths.js";
|
||||
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
||||
|
||||
/* The exit discipline: NEVER process.exit() after writing output. stdout/
|
||||
* stderr to a PIPE are async streams — process.exit() drops whatever libuv
|
||||
* hasn't flushed yet, which truncates large diagnostic renders at the pipe
|
||||
* buffer (observed: 64KB cut mid-code-frame). Every path sets
|
||||
* process.exitCode and returns instead; Node exits naturally once the
|
||||
* streams drain. */
|
||||
class CliExit extends Error {
|
||||
constructor(readonly code: number) {
|
||||
super(`exit ${code}`);
|
||||
this.name = "CliExit";
|
||||
}
|
||||
}
|
||||
|
||||
function fail(msg: string): never {
|
||||
process.stderr.write(msg + "\n");
|
||||
throw new CliExit(1);
|
||||
}
|
||||
|
||||
/** parseArgs, with its throw turned into the CLI's own one-line error.
|
||||
* Unparseable arguments are a USER error — an unknown flag or a missing
|
||||
* value used to reach the top level as an uncaught ERR_PARSE_ARGS_* and
|
||||
* print a Node stack trace over the user's terminal. */
|
||||
function parseCli(args: string[]): ReturnType<typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>> {
|
||||
try {
|
||||
return parseArgs({ args, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
|
||||
} catch (err) {
|
||||
// parseArgs appends a paragraph about `--` and positionals to the
|
||||
// unknown-option message; the first sentence is the part that names
|
||||
// what was wrong, and USAGE below already covers what was meant.
|
||||
const raw = err instanceof Error ? err.message : String(err);
|
||||
const msg = raw.split("\n")[0]!.split(". ")[0]!;
|
||||
fail(`scriptc: ${msg}\n\n${USAGE}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(args: string[], host: CliHost): Promise<number> {
|
||||
const { values, positionals } = parseCli(args);
|
||||
const externalTypeArgs = values["external-types"] ?? [];
|
||||
|
||||
if (values.version) {
|
||||
process.stdout.write(`${host.version()}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
process.stdout.write(USAGE);
|
||||
return values.help ? 0 : 1;
|
||||
}
|
||||
|
||||
const [command, inputArg] = positionals;
|
||||
if (command === "cache") {
|
||||
if (inputArg !== "warm") fail(`unknown cache command "${inputArg ?? ""}" (supported: warm)\n\n${USAGE}`);
|
||||
if (values.lib || values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.ffi !== undefined || values.profile !== undefined || values.strip || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || values["provenance-sources"] || externalTypeArgs.length > 0 || values.out !== undefined || values["emit-ir"] || !values["keep-llvm"]) {
|
||||
fail(`scriptc cache warm takes only native optimization/sanitizer options and profile names\n\n${USAGE}`);
|
||||
}
|
||||
const optimization = values.optimization;
|
||||
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") {
|
||||
fail(`unknown optimization "${optimization}" (supported: release, dev)\n\n${USAGE}`);
|
||||
}
|
||||
const profileArgs = positionals.slice(2);
|
||||
const knownProfiles = new Set<NativeCacheWarmProfile>(["runtime", "tls", "dynamic"]);
|
||||
for (const profile of profileArgs) {
|
||||
if (!knownProfiles.has(profile as NativeCacheWarmProfile)) {
|
||||
fail(`unknown cache warm profile "${profile}" (supported: runtime, tls, dynamic)`);
|
||||
}
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await host.warmNativeCaches({
|
||||
...(optimization === undefined ? {} : { optimization }),
|
||||
sanitize: values.sanitize,
|
||||
...(profileArgs.length === 0
|
||||
? {}
|
||||
: { profiles: profileArgs as NativeCacheWarmProfile[] }),
|
||||
});
|
||||
} catch (error) {
|
||||
fail(`scriptc: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
process.stdout.write(`${result.cacheRoot}\n`);
|
||||
for (const profile of result.profiles) {
|
||||
process.stdout.write(`${profile.profile}\t${Math.round(profile.elapsedMs)}ms\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (command !== "build" && command !== "run" && command !== "coverage") {
|
||||
fail(`unknown command "${command}"\n\n${USAGE}`);
|
||||
}
|
||||
if (values.lib) {
|
||||
// LIBRARY mode: the profile names the entry module and pins the
|
||||
// emission; the executable lane's mode flags have no meaning here
|
||||
// (library artifacts are static-tier only, and there is no fallback
|
||||
// concept — bare npm specifiers are static-or-refuse: the npm-static
|
||||
// eligibility bar runs automatically, eligible packages compile into
|
||||
// the graph, ineligible ones refuse with SC4013).
|
||||
if (command !== "build") fail(`--lib is a build mode (scriptc build --lib --profile <p.json>)\n\n${USAGE}`);
|
||||
const profileArg = values.profile;
|
||||
if (!profileArg) fail(`scriptc build --lib needs --profile <profile.json>\n\n${USAGE}`);
|
||||
if (inputArg) {
|
||||
fail("scriptc build --lib takes no input positional: the profile names the entry module");
|
||||
}
|
||||
if (values.dynamic || values.backend !== undefined || values.emit !== undefined || values.print !== undefined || values.optimization !== undefined || values.strip || values.ffi !== undefined || values["windows-subsystem"] !== undefined || (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--strip/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
|
||||
);
|
||||
}
|
||||
const profilePath = resolve(profileArg);
|
||||
const libOutDir = values.out ? dirname(resolve(values.out)) : join(dirname(profilePath), ".scriptc");
|
||||
const result = await host.compileLibrary({
|
||||
profilePath,
|
||||
outDir: libOutDir,
|
||||
...(values.out ? { outPath: resolve(values.out) } : {}),
|
||||
emitIr: values["emit-ir"],
|
||||
sanitize: values.sanitize,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n");
|
||||
const n = result.diagnostics.length;
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
return 1;
|
||||
}
|
||||
if (!values["keep-llvm"]) rmSync(result.llvmPath, { force: true });
|
||||
process.stdout.write(`${result.archivePath}\n`);
|
||||
// The contract sidecar rides the same invocation when the profile
|
||||
// declares one — name it so the embedder's tooling knows where to look.
|
||||
if (result.sidecarPath !== undefined) process.stdout.write(`${result.sidecarPath}\n`);
|
||||
return 0;
|
||||
}
|
||||
if (values["emit-ir"] && (command === "build" || command === "run")) {
|
||||
process.stderr.write("scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n");
|
||||
}
|
||||
if (!inputArg) fail(`missing input file\n\n${USAGE}`);
|
||||
const input = resolve(inputArg);
|
||||
if (command === "coverage" && values.emit !== undefined) {
|
||||
fail(`--emit is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (command === "coverage" && values.strip) {
|
||||
fail(`--strip is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (values.print !== undefined && values.print !== "native-link-info") {
|
||||
fail(`unknown print kind "${values.print}" (supported: native-link-info)\n\n${USAGE}`);
|
||||
}
|
||||
const printNativeLinkInfo = values.print === "native-link-info";
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
if (printNativeLinkInfo && values.emit !== undefined && values.emit !== "obj") {
|
||||
fail(`--print=native-link-info requires --emit=obj\n\n${USAGE}`);
|
||||
}
|
||||
if (externalTypeArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--external-types is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const externalTypes: Record<string, string> = Object.create(null) as Record<string, string>;
|
||||
for (const mapping of externalTypeArgs) {
|
||||
const equals = mapping.indexOf("=");
|
||||
if (equals <= 0 || equals === mapping.length - 1) {
|
||||
fail(`invalid --external-types mapping ${JSON.stringify(mapping)} (expected <specifier=file.d.ts>)`);
|
||||
}
|
||||
const specifier = mapping.slice(0, equals).trim();
|
||||
const declarationArg = mapping.slice(equals + 1).trim();
|
||||
if (!isExactExternalTypeSpecifier(specifier)) {
|
||||
fail(`invalid --external-types specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`);
|
||||
}
|
||||
if (!/\.d\.(?:ts|mts|cts)$/.test(declarationArg)) {
|
||||
fail(`invalid --external-types declaration ${JSON.stringify(declarationArg)} (expected a .d.ts, .d.mts, or .d.cts file)`);
|
||||
}
|
||||
if (externalTypes[specifier] !== undefined) {
|
||||
fail(`duplicate --external-types mapping for ${JSON.stringify(specifier)}`);
|
||||
}
|
||||
const declarationPath = resolve(declarationArg);
|
||||
try {
|
||||
if (!statSync(declarationPath).isFile()) throw new Error("not a file");
|
||||
} catch {
|
||||
fail(`--external-types declaration does not name a readable file: ${declarationPath}`);
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
}
|
||||
const optimization = values.optimization;
|
||||
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") {
|
||||
fail(`unknown optimization "${optimization}" (supported: release, dev)\n\n${USAGE}`);
|
||||
}
|
||||
const windowsSubsystem = values["windows-subsystem"];
|
||||
if (windowsSubsystem !== undefined && windowsSubsystem !== "console" && windowsSubsystem !== "gui") {
|
||||
fail(`unknown Windows subsystem "${windowsSubsystem}" (supported: console, gui)\n\n${USAGE}`);
|
||||
}
|
||||
if (windowsSubsystem !== undefined && command === "coverage") {
|
||||
fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`);
|
||||
}
|
||||
const output = command === "coverage"
|
||||
? null
|
||||
: resolveOutputOptions(command, {
|
||||
...(values.emit === undefined && !printNativeLinkInfo
|
||||
? {}
|
||||
: { emit: values.emit ?? "obj" }),
|
||||
emitIr: values["emit-ir"],
|
||||
...(values.backend === undefined ? {} : { backend: values.backend }),
|
||||
keepLlvm: values["keep-llvm"],
|
||||
sanitize: values.sanitize,
|
||||
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
|
||||
strip: values.strip,
|
||||
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
|
||||
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
|
||||
});
|
||||
if (output !== null && !output.ok) fail(`${output.message}\n\n${USAGE}`);
|
||||
const backend = output === null ? undefined : output.backend;
|
||||
|
||||
// --npm-static: repeatable and comma-splittable; the literal "auto"
|
||||
// switches to eligibility-based detection (mixing "auto" with names
|
||||
// is rejected — the shapes answer different questions).
|
||||
const npmStaticRaw = (values["npm-static"] ?? []).flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
||||
let npmStatic: string[] | "auto" | undefined;
|
||||
if (npmStaticRaw.includes("auto")) {
|
||||
if (npmStaticRaw.length > 1) fail(`--npm-static auto cannot be combined with package names\n\n${USAGE}`);
|
||||
npmStatic = "auto";
|
||||
} else if (npmStaticRaw.length > 0) {
|
||||
npmStatic = npmStaticRaw;
|
||||
}
|
||||
|
||||
// --provenance-sources resolves BEFORE the program loads (tsgo needs the
|
||||
// source "paths" at creation): attestations and source trees fetch (or
|
||||
// ride the content-addressed cache / the offline manifest), the registry
|
||||
// installs, and every fallback prints as a note — never a failure.
|
||||
const provenance = values["provenance-sources"] ? await host.resolveProvenanceSources(input) : null;
|
||||
if (provenance !== null) {
|
||||
setProvenanceSources(provenance);
|
||||
for (const pkg of provenance.packages) {
|
||||
process.stderr.write(
|
||||
`provenance: ${pkg.name}@${pkg.version} ← ${pkg.repo.replace(/^git\+/, "")} @ ${pkg.commit.slice(0, 12)} (source compiles statically)\n`,
|
||||
);
|
||||
}
|
||||
for (const note of provenance.notes) process.stderr.write(`provenance: ${note}\n`);
|
||||
}
|
||||
|
||||
if (command === "coverage") {
|
||||
const { coverage, sourceTexts } = await host.analyze(input, {
|
||||
dynamic: values.dynamic,
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
});
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(renderCoverage(coverage, { color, sourceTexts }) + "\n");
|
||||
return coverage.preflightFailed ? 1 : 0;
|
||||
}
|
||||
|
||||
if (output === null || !output.ok) throw new Error("internal output-option state");
|
||||
if (windowsSubsystem !== undefined && host.sourceTargetPlatform() !== "win32") {
|
||||
fail(`--windows-subsystem requires a Windows executable target\n\n${USAGE}`);
|
||||
}
|
||||
const { outDir, outPath } = selectOutputPaths(input, output.cliOutputKind, values.out,
|
||||
output.cliOutputKind === "exe" ? host.sourceTargetPlatform() : undefined);
|
||||
|
||||
let nativeLinkInfo: object | undefined;
|
||||
const build = async (): Promise<string> => {
|
||||
const result = await host.compile(input, {
|
||||
outPath,
|
||||
outDir,
|
||||
outputKind: output.outputKind,
|
||||
emitIr: output.emitIr,
|
||||
sanitize: values.sanitize,
|
||||
dynamic: values.dynamic,
|
||||
...(backend !== undefined ? { backend } : {}),
|
||||
...(optimization !== undefined ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true } : {}),
|
||||
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n");
|
||||
const n = result.diagnostics.length;
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
if (result.artifact.kind === "exe") {
|
||||
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
} else if (result.artifact.kind === "obj") {
|
||||
nativeLinkInfo = result.artifact.nativeLinkInfo;
|
||||
}
|
||||
return result.artifact.path;
|
||||
};
|
||||
|
||||
const binary = await build();
|
||||
|
||||
if (command === "run") {
|
||||
return host.run(binary);
|
||||
}
|
||||
|
||||
if (printNativeLinkInfo) {
|
||||
if (nativeLinkInfo === undefined) throw new Error("internal native-link-info state");
|
||||
// Keep stdout pure JSON for tooling; the ordinary artifact path is in
|
||||
// program.object inside the document.
|
||||
process.stdout.write(`${JSON.stringify(nativeLinkInfo, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${binary}\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Both installed and seed commands use this argument and diagnostic contract. */
|
||||
export async function runCli(args: string[], host: CliHost): Promise<number> {
|
||||
try { return await main(args, host); }
|
||||
catch (err) {
|
||||
if (err instanceof Error && err.name === "CliExit") return Number(err.message.slice(5));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { AnalyzeOptions, AnalyzeResult, CompileLibraryOptions, CompileLibraryResult, CompileRequestOptions, CompileRequestResult } from "../compile-types.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
|
||||
export type NativeCacheWarmProfile = "runtime" | "tls" | "dynamic";
|
||||
|
||||
/** Only host operations vary between the seed and installed compiler. */
|
||||
export interface CliHost {
|
||||
version: () => string;
|
||||
sourceTargetPlatform: () => string;
|
||||
analyze: (entry: string, options: AnalyzeOptions) => Promise<AnalyzeResult>;
|
||||
compile: (entry: string, options: CompileRequestOptions) => Promise<CompileRequestResult>;
|
||||
compileLibrary: (options: CompileLibraryOptions) => Promise<CompileLibraryResult>;
|
||||
resolveProvenanceSources: (entry: string) => Promise<ProvenanceSources>;
|
||||
warmNativeCaches: (options: {
|
||||
optimization?: "release" | "dev";
|
||||
sanitize: boolean;
|
||||
profiles?: NativeCacheWarmProfile[];
|
||||
}) => Promise<{ cacheRoot: string; profiles: { profile: NativeCacheWarmProfile; elapsedMs: number }[] }>;
|
||||
run: (binary: string) => Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { CompileOutputKind } from "../compile-types.js";
|
||||
import type { CliOutputKind } from "./paths.js";
|
||||
|
||||
export interface OutputOptionValues {
|
||||
emit?: string;
|
||||
emitIr: boolean;
|
||||
backend?: string;
|
||||
keepLlvm: boolean;
|
||||
sanitize: boolean;
|
||||
optimization?: string;
|
||||
strip?: boolean;
|
||||
windowsSubsystem?: string;
|
||||
ffi?: string;
|
||||
}
|
||||
|
||||
export type OutputOptionResolution =
|
||||
| {
|
||||
ok: true;
|
||||
outputKind: CompileOutputKind;
|
||||
cliOutputKind: CliOutputKind;
|
||||
backend?: "llvm";
|
||||
emitIr: boolean;
|
||||
deprecateEmitIr: boolean;
|
||||
}
|
||||
| { ok: false; message: string };
|
||||
|
||||
const SOURCE_KINDS = new Set<CliOutputKind>(["ir", "llvm"]);
|
||||
const NATIVE_ARTIFACT_KINDS = new Set<CliOutputKind>(["asm", "obj"]);
|
||||
|
||||
/** Pure compatibility/validation matrix for build/run output selection. */
|
||||
export function resolveOutputOptions(
|
||||
command: "build" | "run",
|
||||
values: OutputOptionValues,
|
||||
): OutputOptionResolution {
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
return { ok: false, message: `unknown backend "${values.backend}" (supported: llvm)` };
|
||||
}
|
||||
const backend = values.backend as "llvm" | undefined;
|
||||
const rawEmit = values.emit;
|
||||
if (
|
||||
rawEmit !== undefined && rawEmit !== "ir" && rawEmit !== "llvm" &&
|
||||
rawEmit !== "asm" && rawEmit !== "obj" && rawEmit !== "exe"
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `unknown emit kind "${rawEmit}" (supported: ir, llvm, asm, obj, exe)`,
|
||||
};
|
||||
}
|
||||
const emit = (rawEmit ?? "exe") as CliOutputKind;
|
||||
if (command === "run" && emit !== "exe") {
|
||||
return { ok: false, message: `scriptc run requires --emit=exe` };
|
||||
}
|
||||
if (values.emitIr && rawEmit !== undefined && emit !== "ir" && emit !== "exe") {
|
||||
return { ok: false, message: `--emit-ir cannot be combined with --emit=${emit}; use --emit=ir` };
|
||||
}
|
||||
if (values.emitIr && emit === "ir") {
|
||||
return { ok: false, message: `--emit-ir and --emit=ir select the same output; use --emit=ir` };
|
||||
}
|
||||
if (values.windowsSubsystem !== undefined && emit !== "exe") {
|
||||
return { ok: false, message: `--windows-subsystem is only supported with --emit=exe` };
|
||||
}
|
||||
if (values.strip && emit !== "exe") {
|
||||
return { ok: false, message: `--strip is only supported with --emit=exe` };
|
||||
}
|
||||
if (emit === "ir" && backend !== undefined) {
|
||||
return { ok: false, message: `--emit=ir cannot be combined with --backend; IR is emitted before backend selection` };
|
||||
}
|
||||
if (SOURCE_KINDS.has(emit)) {
|
||||
if (!values.keepLlvm) {
|
||||
return { ok: false, message: `--no-keep-llvm is only meaningful with --emit=exe` };
|
||||
}
|
||||
if (values.sanitize) {
|
||||
return { ok: false, message: `--sanitize is only meaningful with --emit=exe` };
|
||||
}
|
||||
if (values.optimization !== undefined) {
|
||||
return { ok: false, message: `--optimization is only meaningful with --emit=exe` };
|
||||
}
|
||||
}
|
||||
if (NATIVE_ARTIFACT_KINDS.has(emit) && !values.keepLlvm) {
|
||||
return { ok: false, message: `--no-keep-llvm is only meaningful with --emit=exe` };
|
||||
}
|
||||
const outputKind = emit as CompileOutputKind;
|
||||
return {
|
||||
ok: true,
|
||||
outputKind,
|
||||
cliOutputKind: emit,
|
||||
...(emit === "ir" && backend === undefined ? {} : { backend: "llvm" as const }),
|
||||
emitIr: values.emitIr && emit === "exe",
|
||||
deprecateEmitIr: values.emitIr,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { configuredTargetPlatform as sourceTargetPlatform } from "../backend/target-platform.js";
|
||||
export { wasiEnvironment, wasiPreopens } from "./wasi-paths.js";
|
||||
|
||||
export type CliOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
const POSIX_SUFFIXES: Record<CliOutputKind, string> = {
|
||||
ir: ".ir.json",
|
||||
llvm: ".ll",
|
||||
asm: ".s",
|
||||
obj: ".o",
|
||||
exe: "",
|
||||
};
|
||||
|
||||
const WINDOWS_SUFFIXES: Record<CliOutputKind, string> = {
|
||||
...POSIX_SUFFIXES,
|
||||
asm: ".asm",
|
||||
obj: ".obj",
|
||||
exe: ".exe",
|
||||
};
|
||||
|
||||
export function defaultOutputName(
|
||||
stem: string,
|
||||
kind: CliOutputKind,
|
||||
platform?: string,
|
||||
): string {
|
||||
const selectedPlatform = platform ?? (kind === "exe" ? sourceTargetPlatform() : process.platform);
|
||||
if (kind === "exe" && selectedPlatform === "wasi") return `${stem}.wasm`;
|
||||
return `${stem}${(selectedPlatform === "win32" ? WINDOWS_SUFFIXES : POSIX_SUFFIXES)[kind]}`;
|
||||
}
|
||||
|
||||
export interface OutputPaths {
|
||||
outDir: string;
|
||||
outPath: string;
|
||||
}
|
||||
|
||||
/** One authority for explicit and default primary artifact paths. */
|
||||
export function selectOutputPaths(
|
||||
input: string,
|
||||
kind: CliOutputKind,
|
||||
explicitOut?: string,
|
||||
platform?: string,
|
||||
): OutputPaths {
|
||||
const absoluteInput = resolve(input);
|
||||
const outDir = explicitOut === undefined
|
||||
? join(dirname(absoluteInput), ".scriptc")
|
||||
: dirname(resolve(explicitOut));
|
||||
const stem = basename(absoluteInput).replace(/\.(ts|mts|cts|js|mjs|cjs|c|ll)$/, "");
|
||||
return {
|
||||
outDir,
|
||||
outPath: explicitOut === undefined
|
||||
? join(outDir, defaultOutputName(stem, kind, platform))
|
||||
: resolve(explicitOut),
|
||||
};
|
||||
}
|
||||
|
||||
/** Default executable filename for the build target. Explicit --out paths
|
||||
* stay exact; only scriptc's generated default needs the Windows PE suffix. */
|
||||
export function defaultExecutableName(stem: string, platform: string = sourceTargetPlatform()): string {
|
||||
return defaultOutputName(stem, "exe", platform);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export const USAGE = `scriptc — TypeScript/JavaScript to native and WebAssembly executables (experimental)
|
||||
|
||||
Usage:
|
||||
scriptc build <file.ts|.js> [options] compile to an executable or source artifact
|
||||
scriptc run <file.ts|.js> [options] compile and run
|
||||
scriptc coverage <file.ts|.js> how much compiles statically, and why not
|
||||
scriptc coverage <file.ts|.js> --dynamic what a --dynamic build compiles, and what still blocks it
|
||||
scriptc coverage <file.ts|.js> --external-types <specifier=file.d.ts>
|
||||
type-resolve an embedder-provided module for analysis
|
||||
scriptc build --lib --profile <p.json> library mode: compile the profile's entry
|
||||
module to a linkable static archive
|
||||
(<name>.lib.a), or a Wasm reactor
|
||||
(<name>.wasm) on wasm32-wasi,
|
||||
exporting the profile symbols; a profile
|
||||
with a sidecar section also gets the
|
||||
contract sidecar JSON beside the archive
|
||||
scriptc cache warm [runtime|tls|dynamic…] prebuild expensive native cache families
|
||||
for the current compiler/SDK/target
|
||||
|
||||
Options:
|
||||
-o, --out <path> primary output path (default: .scriptc/<name><suffix>)
|
||||
--emit <kind> primary output: ir, llvm, asm, obj, or exe
|
||||
(default: exe). asm/obj use the matching platform helper
|
||||
--print <kind> print machine-readable metadata instead of the output path
|
||||
(native-link-info implies --emit=obj and never links)
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
uses -O0, source breakpoints, and cached LLVM object shards;
|
||||
macOS executable builds also produce an adjacent .dSYM
|
||||
--strip remove symbol/debug payload from the linked executable
|
||||
for smaller builds (opt in; --emit=exe only)
|
||||
--windows-subsystem <console|gui>
|
||||
Windows executable subsystem (default: console). gui
|
||||
prevents Windows from opening a console window
|
||||
--keep-llvm keep generated LLVM IR beside the executable (default)
|
||||
--no-keep-llvm delete generated LLVM IR after compiling
|
||||
--emit-ir also write IR beside an executable or library archive;
|
||||
deprecated for executables: use --emit=ir for primary IR
|
||||
--sanitize build with ASan + runtime RC audit
|
||||
--dynamic embed the dynamic engine (adds ~620KB; static stays the default)
|
||||
--ffi <file> bind signature-only TypeScript declarations to native
|
||||
C symbols and link the manifest's archives/libraries
|
||||
--npm-static <pkg[,pkg…]|auto>
|
||||
compile the named npm packages' shipped JS statically as
|
||||
program modules (repeatable; "auto" opts in every eligible
|
||||
direct import: own .d.ts, unminified JS, no build-transform
|
||||
markers). A package preflight refuses falls back to the
|
||||
island (--dynamic) with a coverage-report note — opt-in,
|
||||
experimental
|
||||
--provenance-sources
|
||||
EXPERIMENTAL: compile npm dependencies from their
|
||||
provenance-attested SOURCE (fetched at the attested
|
||||
commit) as static program modules; packages without a
|
||||
usable attestation keep the island path (a note, never
|
||||
a failure)
|
||||
--external-types <specifier=file.d.ts>
|
||||
coverage only: map an exact bare module specifier to a
|
||||
local declaration file. The declaration supplies types
|
||||
for analysis; the host module remains an explicit
|
||||
external-boundary blocker (repeatable)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
|
||||
export const CLI_OPTIONS = {
|
||||
out: { type: "string", short: "o" },
|
||||
emit: { type: "string" },
|
||||
print: { type: "string" },
|
||||
backend: { type: "string" },
|
||||
optimization: { type: "string" },
|
||||
strip: { type: "boolean", default: false },
|
||||
"windows-subsystem": { type: "string" },
|
||||
"keep-llvm": { type: "boolean", default: true },
|
||||
"emit-ir": { type: "boolean", default: false },
|
||||
sanitize: { type: "boolean", default: false },
|
||||
dynamic: { type: "boolean", default: false },
|
||||
ffi: { type: "string" },
|
||||
"npm-static": { type: "string", multiple: true },
|
||||
"provenance-sources": { type: "boolean", default: false },
|
||||
"external-types": { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
version: { type: "boolean", short: "v", default: false },
|
||||
} as const;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { wasiGuestPath } from "../wasi-paths.js";
|
||||
|
||||
/** Host paths exposed by `scriptc run` to a WASI Preview 1 module. Guest
|
||||
* `/tmp` maps to the host's real platform temp directory instead of assuming
|
||||
* the POSIX spelling exists (notably false on Windows). */
|
||||
export function wasiPreopens(
|
||||
cwd: string = process.cwd(),
|
||||
hostTmp: string = tmpdir(),
|
||||
): Record<string, string> {
|
||||
return { "/": cwd, "/tmp": hostTmp };
|
||||
}
|
||||
|
||||
/** Environment inherited by a WASI module. Host-absolute directory values
|
||||
* must not claim paths outside the guest namespace: `/` is the module's
|
||||
* capability root/home/cwd and `/tmp` is its writable temporary directory. */
|
||||
export function wasiEnvironment(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
cwd: string = process.cwd(),
|
||||
hostTmp: string = tmpdir(),
|
||||
): Record<string, string> {
|
||||
const guest: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(env)) {
|
||||
if (value !== undefined) guest[name] = value;
|
||||
}
|
||||
|
||||
guest["PWD"] = "/";
|
||||
guest["HOME"] = "/";
|
||||
guest["TMPDIR"] = "/tmp";
|
||||
if (guest["USERPROFILE"] !== undefined) guest["USERPROFILE"] = "/";
|
||||
if (guest["TMP"] !== undefined) guest["TMP"] = "/tmp";
|
||||
if (guest["TEMP"] !== undefined) guest["TEMP"] = "/tmp";
|
||||
|
||||
// These optional shell/package-manager paths retain their meaning only
|
||||
// when they fall under a capability the runner actually exposes.
|
||||
for (const key of ["OLDPWD", "INIT_CWD"]) {
|
||||
const value = guest[key];
|
||||
if (value === undefined) continue;
|
||||
const mapped = wasiGuestPath(value, cwd, hostTmp);
|
||||
if (mapped === null) delete guest[key];
|
||||
else guest[key] = mapped;
|
||||
}
|
||||
|
||||
return guest;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
/* The subprocess host for `scriptc run` on wasm32-wasi. Keeping the WASI
|
||||
* instance outside the CLI process preserves native run's exit isolation:
|
||||
* process.exit(), traps, and signals cannot take the compiler process down.
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { WASI } from "node:wasi";
|
||||
import { wasiGuestPath } from "../wasi-paths.js";
|
||||
import { wasiEnvironment, wasiPreopens } from "./wasi-paths.js";
|
||||
|
||||
type WasiInstance = Parameters<WASI["start"]>[0];
|
||||
declare const WebAssembly: {
|
||||
instantiate(
|
||||
bytes: Uint8Array,
|
||||
imports: ReturnType<WASI["getImportObject"]>,
|
||||
): Promise<{ instance: WasiInstance }>;
|
||||
};
|
||||
|
||||
const binary = process.argv[2];
|
||||
if (binary === undefined) throw new Error("scriptc WASI runner needs a module path");
|
||||
const cwd = process.cwd();
|
||||
const hostTmp = tmpdir();
|
||||
|
||||
const wasi = new WASI({
|
||||
version: "preview1",
|
||||
args: [wasiGuestPath(binary, cwd, hostTmp) ?? binary],
|
||||
env: wasiEnvironment(process.env, cwd, hostTmp),
|
||||
// A native scriptc executable inherits access to the caller's filesystem.
|
||||
// WASI is capability-based, so expose the caller's working tree as `/`
|
||||
// and the platform's actual temporary directory as guest `/tmp`.
|
||||
preopens: wasiPreopens(cwd, hostTmp),
|
||||
returnOnExit: true,
|
||||
});
|
||||
const instantiated = await WebAssembly.instantiate(
|
||||
await readFile(binary),
|
||||
wasi.getImportObject(),
|
||||
);
|
||||
process.exitCode = wasi.start(instantiated.instance);
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { WindowsSubsystem } from "./backend/targets.js";
|
||||
import type { NativeLinkInfo } from "./backend/native-link-info.js";
|
||||
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
|
||||
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
export interface CompileBaseOptions {
|
||||
/** Primary artifact path. The CLI supplies the output-kind default. */
|
||||
outPath: string;
|
||||
/** Where generated intermediates and compatibility side artifacts land. */
|
||||
outDir: string;
|
||||
/** @deprecated This option no longer controls output cleanup; sibling artifacts are retained. */
|
||||
defaultOutputPath?: boolean;
|
||||
/** Compatibility-only additive IR side artifact for executable builds.
|
||||
* The CLI's deprecated --emit-ir flag supplies this option. */
|
||||
emitIr?: boolean;
|
||||
sanitize?: boolean;
|
||||
/** Embed the dynamic-island engine (--dynamic). Off = the static default:
|
||||
* island constructs are diagnostics and nothing about codegen or linking
|
||||
* changes. */
|
||||
dynamic?: boolean;
|
||||
/** LLVM is the production code generator. */
|
||||
backend?: "llvm";
|
||||
/** Native optimization posture. Release is the shipped -O2 default; dev
|
||||
* uses -O0, source line tables, and stable multi-TU object caching for
|
||||
* large LLVM programs. Darwin executables include an adjacent .dSYM. */
|
||||
optimization?: "release" | "dev";
|
||||
/** Remove symbol/debug payload from an executable at link time. */
|
||||
strip?: boolean;
|
||||
/** Windows PE executable subsystem. Console is the default; GUI suppresses
|
||||
* automatic console-window creation. Only valid for Windows executables. */
|
||||
windowsSubsystem?: WindowsSubsystem;
|
||||
/** --npm-static: package names whose shipped, unminified JS compiles
|
||||
* STATICALLY as program modules (inference types the bodies; statements
|
||||
* the lowering cannot prove become runtime fences). "auto" opts in every
|
||||
* directly-imported package passing the eligibility heuristics (own
|
||||
* .d.ts, unminified JS, no build-transform markers). A package whose
|
||||
* preflight refuses marks itself an offender and falls back to the
|
||||
* island (--dynamic) or the requires-dynamic diagnostic (static builds)
|
||||
* — never a silent misbuild. Off by default: nothing changes without
|
||||
* the flag. */
|
||||
npmStatic?: readonly string[] | "auto";
|
||||
/** Outbound native FFI manifest. Its signature-only TypeScript bindings
|
||||
* lower to direct C ABI calls. Source outputs retain those declarations;
|
||||
* archive/system-library inputs join only an executable link. */
|
||||
ffiProfilePath?: string;
|
||||
/** Attach the machine-readable external link recipe to an object result.
|
||||
* Valid only with outputKind "obj"; it never invokes a linker. */
|
||||
nativeLinkInfo?: boolean;
|
||||
}
|
||||
|
||||
/** Executable compile options. This remains the compatibility type for the
|
||||
* historical compile() API, whose omitted output kind means executable. */
|
||||
export interface CompileOptions extends CompileBaseOptions {
|
||||
outputKind?: "exe";
|
||||
/** Internal validation lane retained for helper-object artifact tests.
|
||||
* Supported ordinary LLVM executable builds select this path automatically. */
|
||||
nativeProgramObject?: boolean;
|
||||
}
|
||||
|
||||
/** Source-artifact compile options, discriminated by the required kind. */
|
||||
export interface CompileSourceOptions extends CompileBaseOptions {
|
||||
outputKind: Exclude<CompileOutputKind, "exe">;
|
||||
}
|
||||
|
||||
/** Internal/dynamic request shape for callers that select the kind at runtime.
|
||||
* Statically executable/source callers should prefer the narrower interfaces. */
|
||||
export interface CompileRequestOptions extends CompileBaseOptions {
|
||||
outputKind?: CompileOutputKind;
|
||||
/** Internal validation lane for executable requests. */
|
||||
nativeProgramObject?: boolean;
|
||||
}
|
||||
|
||||
export type CompileArtifact =
|
||||
| { kind: "ir"; path: string }
|
||||
| { kind: "llvm"; path: string }
|
||||
| { kind: "asm"; path: string }
|
||||
| { kind: "obj"; path: string; nativeLinkInfo?: NativeLinkInfo }
|
||||
| {
|
||||
kind: "exe";
|
||||
path: string;
|
||||
translationUnitPath: string;
|
||||
backend: "llvm";
|
||||
|
||||
};
|
||||
|
||||
export type CompileFailure = {
|
||||
ok: false;
|
||||
diagnostics: ScrDiagnostic[];
|
||||
sourceTexts: Map<string, string>;
|
||||
};
|
||||
|
||||
export type CompileSourceResult =
|
||||
| { ok: true; artifact: Extract<CompileArtifact, { kind: "ir" | "llvm" | "asm" | "obj" }> }
|
||||
| CompileFailure;
|
||||
|
||||
/** Historical executable result shape retained for source compatibility. */
|
||||
export type CompileResult =
|
||||
| {
|
||||
ok: true;
|
||||
binaryPath: string;
|
||||
llvmPath: string;
|
||||
irPath?: string;
|
||||
backend: "llvm";
|
||||
|
||||
}
|
||||
| CompileFailure;
|
||||
|
||||
export type CompileExecutableResult =
|
||||
/** The generated LLVM source is retained beside the executable. */
|
||||
| (Extract<CompileResult, { ok: true }> & {
|
||||
artifact: Extract<CompileArtifact, { kind: "exe" }>;
|
||||
})
|
||||
| CompileFailure;
|
||||
|
||||
/** Result union for callers that choose outputKind dynamically. */
|
||||
export type CompileRequestResult = CompileSourceResult | CompileExecutableResult;
|
||||
|
||||
export interface CompileLibraryOptions {
|
||||
profilePath: string;
|
||||
/** Where the archive and the kept program TU land. */
|
||||
outDir: string;
|
||||
/** Artifact path. Default: <stem>.lib.a, or <stem>.wasm for wasm32-wasi. */
|
||||
outPath?: string;
|
||||
emitIr?: boolean;
|
||||
sanitize?: boolean;
|
||||
}
|
||||
|
||||
export type CompileLibraryResult =
|
||||
/** `sidecarPath` is present exactly when the profile declares a
|
||||
* `sidecar` section: the contract JSON written beside the archive by
|
||||
* the same invocation (ask 2). */
|
||||
| { ok: true; archivePath: string; llvmPath: string; backend: "llvm"; irPath?: string; sidecarPath?: string }
|
||||
| { ok: false; diagnostics: ScrDiagnostic[]; sourceTexts: Map<string, string> };
|
||||
|
||||
export interface AnalyzeOptions {
|
||||
/** Analyze as a --dynamic build (island constructs lower instead of
|
||||
* producing requires-dynamic diagnostics). */
|
||||
dynamic?: boolean;
|
||||
/** --npm-static (see CompileOptions.npmStatic): the analysis compiles
|
||||
* opted-in packages' JS as program modules and the coverage report
|
||||
* carries each package's static/fallback status. */
|
||||
npmStatic?: readonly string[] | "auto";
|
||||
/** Analyze with the outbound native bindings from this FFI manifest. */
|
||||
ffiProfilePath?: string;
|
||||
/** Coverage-only external host type surfaces: exact bare module
|
||||
* specifier → local declaration file. The checker uses the declarations
|
||||
* to analyze project code, but imported runtime values remain explicit
|
||||
* SC1010 blockers rather than being counted as executable. */
|
||||
externalTypes?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
coverage: CoverageInput;
|
||||
sourceTexts: Map<string, string>;
|
||||
}
|
||||
@@ -190,21 +190,21 @@ export function renderCoverage(input: CoverageInput, opts: { color?: boolean; so
|
||||
// surface as SC2030 blockers below; "lazy trap" rows DON'T fail the
|
||||
// build — the binary embeds Node's call-time error and the call throws
|
||||
// at runtime, exactly where Node would have failed.
|
||||
const builtins = (input.dynamic && input.npmBuiltins) || [];
|
||||
const traps = (input.dynamic && input.npmLazyTraps) || [];
|
||||
const builtins = input.dynamic ? input.npmBuiltins ?? [] : [];
|
||||
const traps = input.dynamic ? input.npmLazyTraps ?? [] : [];
|
||||
if (builtins.length > 0 || traps.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, traps.length > 0 ? "embedded npm code imports Node builtins and unresolved specifiers:" : "embedded npm code imports Node builtins:")}`,
|
||||
);
|
||||
const widestB = Math.max(
|
||||
const widestB = Math.max(...[
|
||||
...builtins.map((b) => b.builtin.length),
|
||||
...traps.map((t) => t.specifier.length),
|
||||
);
|
||||
]);
|
||||
// Pad the plain words before coloring — escape codes have no width.
|
||||
const widestS = Math.max(
|
||||
const widestS = Math.max(...[
|
||||
...builtins.map((b) => (b.shimmed ? 7 : b.lazy ? 23 : 11)),
|
||||
...traps.map(() => 24),
|
||||
);
|
||||
]);
|
||||
for (const b of builtins) {
|
||||
const status = b.shimmed
|
||||
? c(GREEN, "shimmed".padEnd(widestS))
|
||||
@@ -247,7 +247,7 @@ export function renderCoverage(input: CoverageInput, opts: { color?: boolean; so
|
||||
let pFailed = 0;
|
||||
let pIsland = 0;
|
||||
const dir = pkg.dir.endsWith("/") ? pkg.dir : `${pkg.dir}/`;
|
||||
for (const [file, s] of input.statsByFile ?? []) {
|
||||
for (const [file, s] of input.statsByFile ?? new Map<string, { total: number; failed: number; island: number }>()) {
|
||||
if (!file.startsWith(dir)) continue;
|
||||
pTotal += s.total;
|
||||
pFailed += s.failed;
|
||||
|
||||
@@ -187,7 +187,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
["for-using-of", "for (using ... of ...) over arrays", "each array element is disposed at the end of its iteration, including break and continue paths"],
|
||||
["finally-abrupt-completions", "abrupt completions through finally", "return, throw, break, continue, and labeled jumps run crossed finally blocks; a finally completion replaces the pending one"],
|
||||
["spread-arguments", "spread arguments", "non-empty fixed tuples flatten into fixed signatures with evaluate-once ordering; arrays, Sets, and statically represented class iterables spread into typed rest parameters"],
|
||||
] as const) {
|
||||
] as [string, string, string][]) {
|
||||
add({ id: `syntax.${id}`, kind: "syntax", name, status: "static", note });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { CompileFailure, CompileRequestOptions } from "../compile-types.js";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { checkerPanicDiag, iceDiag, isCheckerPanic, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { IrModule, SrcLoc } from "../ir/ir.js";
|
||||
import { validateModule } from "../ir/validate.js";
|
||||
import { moduleWasiUnavailableSurface, targetRefusalDiag } from "../backend/target-diagnostics.js";
|
||||
import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
|
||||
export interface PreparedExecutableModule {
|
||||
ok: true;
|
||||
mod: IrModule;
|
||||
sourceTexts: Map<string, string>;
|
||||
stats: LowerStats;
|
||||
}
|
||||
|
||||
/** Release the parser/checker before native object generation begins. */
|
||||
export function prepareExecutableModule(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
buildPlatform: string,
|
||||
createFrontend: FrontendFactory,
|
||||
timing: CompilationTiming = () => {},
|
||||
): PreparedExecutableModule | CompileFailure {
|
||||
const fe = createFrontend(entryPath, opts.npmStatic);
|
||||
timing("frontend-load");
|
||||
let lowered: LowerResult;
|
||||
let sourceTexts: Map<string, string>;
|
||||
// The frontend (and its tsgo server) is released as soon as lowering
|
||||
// ends — clang and the link never hold it open.
|
||||
try {
|
||||
const fail = (diagnostics: ScrDiagnostic[]): CompileFailure => ({
|
||||
ok: false,
|
||||
diagnostics,
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
});
|
||||
|
||||
if (fe.preflight.length > 0) return fail(fe.preflight);
|
||||
|
||||
try {
|
||||
lowered = fe.lower({
|
||||
dynamic: opts.dynamic ?? false,
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
});
|
||||
timing("lower");
|
||||
} catch (e) {
|
||||
// The last-resort panic fence: an upstream tsgo panic that crossed a
|
||||
// checker call no statement/collection fence wrapped still becomes a
|
||||
// clean failed compile (anchored at the entry), never a crashed CLI.
|
||||
if (!isCheckerPanic(e)) throw e;
|
||||
return fail([
|
||||
checkerPanicDiag(e.message.split("\n", 1)[0]!, { file: entryPath, start: 0, end: 0 }),
|
||||
]);
|
||||
}
|
||||
if (lowered.module === null) return fail(lowered.diagnostics);
|
||||
|
||||
const validation = validateModule(lowered.module);
|
||||
timing("ir-validate");
|
||||
if (validation.length > 0) {
|
||||
return fail(validation.map((v) => iceDiag(v.message, v.loc)));
|
||||
}
|
||||
if (buildPlatform === "wasi") {
|
||||
const entryLoc: SrcLoc = { file: entryPath, start: 0, end: 0 };
|
||||
if (opts.sanitize) {
|
||||
return fail([targetRefusalDiag("wasm32-wasi", "--sanitize", entryLoc)]);
|
||||
}
|
||||
if (ffi !== null) {
|
||||
return fail([targetRefusalDiag("wasm32-wasi", "native FFI manifests", entryLoc)]);
|
||||
}
|
||||
const unavailable = moduleWasiUnavailableSurface(lowered.module);
|
||||
if (unavailable !== null) {
|
||||
return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]);
|
||||
}
|
||||
}
|
||||
sourceTexts = fe.sourceTexts();
|
||||
} finally {
|
||||
fe.dispose();
|
||||
timing("frontend-dispose");
|
||||
}
|
||||
|
||||
return { ok: true, mod: lowered.module!, sourceTexts, stats: lowered.stats };
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { provenanceSources } from "./provenance-registry.js";
|
||||
import type { FrontendFactory } from "./pipeline.js";
|
||||
|
||||
/** Analysis without codegen: how much of the program compiles statically.
|
||||
* Unlike compile(), lowering diagnostics are data here, not failure. */
|
||||
export function analyzeWithFrontend(entryPath: string, opts: AnalyzeOptions, buildPlatform: string, createFrontend: FrontendFactory): AnalyzeResult {
|
||||
let ffi: FfiProfile | null = null;
|
||||
if (opts.ffiProfilePath !== undefined) {
|
||||
const loaded = loadFfiProfile(opts.ffiProfilePath);
|
||||
if (!loaded.ok) {
|
||||
return {
|
||||
coverage: {
|
||||
file: entryPath,
|
||||
dynamic: opts.dynamic ?? false,
|
||||
stats: { statementsTotal: 0, statementsFailed: 0, statementsIsland: 0, functionsSkipped: 0 },
|
||||
diagnostics: loaded.diagnostics,
|
||||
preflightFailed: true,
|
||||
},
|
||||
sourceTexts: new Map(),
|
||||
};
|
||||
}
|
||||
ffi = loaded.profile;
|
||||
}
|
||||
const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes);
|
||||
try {
|
||||
const emptyStats = { statementsTotal: 0, statementsFailed: 0, statementsIsland: 0, functionsSkipped: 0 };
|
||||
|
||||
const preflight = fe.preflight;
|
||||
// Import-FORM fences don't stop the analysis: the module graph is still
|
||||
// computable (a fenced import contributes no edges), the imported
|
||||
// bindings poison at their use sites, and the fences join the blockers
|
||||
// list beside statement-level ones — the report shows a statement
|
||||
// percentage instead of stopping at the import lines. Everything else —
|
||||
// tsc errors, config incompatibilities, circular imports — still stops
|
||||
// at preflight (no trustworthy program to lower). Builds are unchanged:
|
||||
// compile() fails on every preflight diagnostic exactly as before.
|
||||
const IMPORT_FENCES = new Set(["SC1010", "SC1012", "SC1013", "SC1014", "SC1015"]);
|
||||
if (preflight.some((d) => !IMPORT_FENCES.has(d.code))) {
|
||||
return {
|
||||
coverage: {
|
||||
file: entryPath,
|
||||
dynamic: opts.dynamic ?? false,
|
||||
stats: emptyStats,
|
||||
diagnostics: preflight,
|
||||
...(fe.npmStatic.length > 0 ? { npmStatic: fe.npmStatic } : {}),
|
||||
preflightFailed: true,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
};
|
||||
}
|
||||
// Coverage is whole-program by design: builds stop at what the entry
|
||||
// reaches, but the analysis additionally lowers the unreached remainder
|
||||
// (throwaway) so the report covers everything the source declares — with
|
||||
// the unreached share in its own group.
|
||||
const lowered = fe.lower({
|
||||
dynamic: opts.dynamic ?? false,
|
||||
coverage: true,
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
coverage: {
|
||||
file: entryPath,
|
||||
dynamic: opts.dynamic ?? false,
|
||||
stats: lowered.stats,
|
||||
// The import fences report as blockers alongside the statement-level
|
||||
// ones (use sites of the fenced bindings emit matching diagnostics,
|
||||
// which the report groups with these).
|
||||
diagnostics: [...preflight, ...lowered.diagnostics],
|
||||
...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}),
|
||||
...(lowered.unreached ? { unreached: lowered.unreached } : {}),
|
||||
...(lowered.npmBuiltins ? { npmBuiltins: lowered.npmBuiltins } : {}),
|
||||
...(lowered.npmLazyTraps ? { npmLazyTraps: lowered.npmLazyTraps } : {}),
|
||||
...(fe.npmStatic.length > 0 ? { npmStatic: fe.npmStatic } : {}),
|
||||
// --provenance-sources: the per-package attribution inputs (the
|
||||
// report aggregates statsByFile under each package's source dir).
|
||||
...(provenance !== null ? { provenance } : {}),
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
};
|
||||
} finally {
|
||||
fe.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import vm from "node:vm";
|
||||
import ts5 from "typescript5";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** The Node client's compile-time evaluator. Only source text and the
|
||||
* resulting value cross this boundary; TS5 ASTs never enter the frontend.
|
||||
* Every invocation gets fresh globals and a bounded execution window. */
|
||||
export function evaluateNodeComptime(source: string, timeoutMs: number): unknown {
|
||||
const ts5 = require("typescript5") as typeof import("typescript5");
|
||||
const js = ts5.transpileModule(`(${source})()`, {
|
||||
compilerOptions: { target: ts5.ScriptTarget.ESNext },
|
||||
}).outputText;
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { npmPackageNameOf } from "./workspace-registry.js";
|
||||
import { tsgoPath } from "./ts7/session-path.js";
|
||||
|
||||
export { tsgoPath } from "./ts7/session-path.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let installedDeclarationRoot: string | null = null;
|
||||
|
||||
/** A native distribution supplies its relocatable declaration directory. */
|
||||
export function setDeclarationRoot(root: string): void { installedDeclarationRoot = root; }
|
||||
|
||||
function declarationPath(name: string): string {
|
||||
return tsgoPath(installedDeclarationRoot === null
|
||||
? name === "scriptc.d.ts" ? require.resolve("@scriptc/compiler/scriptc.d.ts")
|
||||
: name === "scriptc-overrides.d.ts" ? require.resolve("@scriptc/compiler/scriptc-overrides.d.ts")
|
||||
: require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts")
|
||||
: join(installedDeclarationRoot, name));
|
||||
}
|
||||
|
||||
/** Path of the shipped ambient declarations — the always-shipped CORE
|
||||
* (comptime/__island_eval, setTimeout). Part of EVERY program scriptc
|
||||
* builds, the project-world preflight program included. */
|
||||
export function ambientDtsPath(): string {
|
||||
return tsgoPath(require.resolve("@scriptc/compiler/scriptc.d.ts"));
|
||||
return declarationPath("scriptc.d.ts");
|
||||
}
|
||||
|
||||
/** Path of the shipped divergence/precision OVERRIDES (JSON.parse():
|
||||
@@ -19,7 +32,7 @@ export function ambientDtsPath(): string {
|
||||
* so a project that typechecks under its own tsc never fails preflight over
|
||||
* an override-manufactured error (checkPreflight). */
|
||||
export function overridesDtsPath(): string {
|
||||
return tsgoPath(require.resolve("@scriptc/compiler/scriptc-overrides.d.ts"));
|
||||
return declarationPath("scriptc-overrides.d.ts");
|
||||
}
|
||||
|
||||
/** Path of the shipped FALLBACK declarations (console, process, node:fs) —
|
||||
@@ -27,7 +40,7 @@ export function overridesDtsPath(): string {
|
||||
* With @types/node, the project's real Node types stand in and this file
|
||||
* stands down (its declaration forms would collide). */
|
||||
export function fallbackDtsPath(): string {
|
||||
return tsgoPath(require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts"));
|
||||
return declarationPath("scriptc-node-fallback.d.ts");
|
||||
}
|
||||
|
||||
/** True for files belonging to the adopted Node type surface: the
|
||||
|
||||
@@ -176,3 +176,27 @@ test("failed directory enumeration invalidates when access is restored", async (
|
||||
await chmod(packages, 0o700);
|
||||
}
|
||||
});
|
||||
|
||||
test("synchronous tracking restores parents after throws and propagates unstable inputs", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const first = join(dir, "first.ts");
|
||||
const second = join(dir, "second.ts");
|
||||
await writeFile(first, "export const first = 1;");
|
||||
await writeFile(second, "export const second = 2;");
|
||||
const parent = new FrontendInputTracker();
|
||||
const child = new FrontendInputTracker();
|
||||
parent.runSynchronous(() => {
|
||||
expect(() => child.runSynchronous(() => {
|
||||
trackedReadFile(first);
|
||||
markFrontendInputsUnstable();
|
||||
throw new Error("interrupted");
|
||||
})).toThrow("interrupted");
|
||||
trackedReadFile(second);
|
||||
});
|
||||
expect(parent.snapshot().probes.filter((probe) => probe.op === "file").map((probe) => probe.path)).toEqual([first, second]);
|
||||
expect(parent.snapshot().stable).toBe(false);
|
||||
const before = parent.snapshot();
|
||||
trackedFileExists(join(dir, "outside.ts"));
|
||||
expect(parent.snapshot()).toEqual(before);
|
||||
});
|
||||
|
||||
@@ -41,8 +41,8 @@ export interface FrontendSemanticMatch {
|
||||
* named artifacts and a generated directory's formerly-missing observation
|
||||
* are excluded. */
|
||||
export interface FrontendInputExclusions {
|
||||
outputPaths?: Iterable<string>;
|
||||
outputDirectories?: Iterable<string>;
|
||||
outputPaths?: readonly string[];
|
||||
outputDirectories?: readonly string[];
|
||||
}
|
||||
|
||||
function frontendSourceDigest(text: string): string {
|
||||
@@ -73,6 +73,7 @@ function systemRealpath(path: string): string {
|
||||
}
|
||||
|
||||
const activeTracker = new AsyncLocalStorage<FrontendInputTracker>();
|
||||
let synchronousTracker: FrontendInputTracker | undefined;
|
||||
|
||||
export class FrontendInputTracker {
|
||||
private readonly probes = new Map<string, FrontendInputProbe>();
|
||||
@@ -88,6 +89,23 @@ export class FrontendInputTracker {
|
||||
});
|
||||
}
|
||||
|
||||
/** The native frontend is synchronous; its typed result need not cross an
|
||||
* async-context callback boundary. Nested trackers still propagate probes. */
|
||||
runSynchronous<T>(fn: () => T): T {
|
||||
const parent = synchronousTracker;
|
||||
// Install the active tracker so filesystem callbacks can record probes.
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
synchronousTracker = this;
|
||||
try { return fn(); }
|
||||
finally {
|
||||
synchronousTracker = parent;
|
||||
if (parent !== undefined && parent !== this) {
|
||||
for (const probe of this.probes.values()) parent.record(probe);
|
||||
if (!this.stable) parent.markUnstable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record(probe: FrontendInputProbe): void {
|
||||
const key = `${probe.op}\0${probe.path}`;
|
||||
const previous = this.probes.get(key);
|
||||
@@ -140,13 +158,13 @@ export class FrontendInputTracker {
|
||||
}
|
||||
|
||||
function record(probe: FrontendInputProbe): void {
|
||||
activeTracker.getStore()?.record(probe);
|
||||
(synchronousTracker ?? activeTracker.getStore())?.record(probe);
|
||||
}
|
||||
|
||||
/** Mark the active frontend as unsafe to persist because an exact host query
|
||||
* bypassed the tracked filesystem wrappers. No-op outside a frontend run. */
|
||||
export function markFrontendInputsUnstable(): void {
|
||||
activeTracker.getStore()?.markUnstable();
|
||||
(synchronousTracker ?? activeTracker.getStore())?.markUnstable();
|
||||
}
|
||||
|
||||
export function trackedReadFile(path: string): string | null {
|
||||
@@ -411,11 +429,11 @@ export function frontendInputsSemanticallyMatch(
|
||||
/** Pure validator used by the persistent-cache reader before any path probes. */
|
||||
export function validFrontendInputSnapshot(snapshot: unknown): snapshot is FrontendInputSnapshot {
|
||||
if (snapshot === null || typeof snapshot !== "object") return false;
|
||||
const candidate = snapshot as Partial<FrontendInputSnapshot>;
|
||||
const candidate = snapshot as { version?: unknown; stable?: unknown; probes?: unknown };
|
||||
if (candidate.version !== 1 || candidate.stable !== true || !Array.isArray(candidate.probes)) return false;
|
||||
return candidate.probes.every((probe) => {
|
||||
if (probe === null || typeof probe !== "object") return false;
|
||||
const value = probe as Partial<FrontendInputProbe>;
|
||||
const value = probe as Record<string, unknown>;
|
||||
if (typeof value.path !== "string" || typeof value.op !== "string") return false;
|
||||
switch (value.op) {
|
||||
case "file":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BOOL, F64, type IrExpr, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, NULL_T, type IrExpr, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import { numLit, varRef } from "../../ir/build.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -40,8 +40,12 @@ export function lowerArrayFill(
|
||||
arrType: IrType & { kind: "array" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const valueType = writeUndefined ? F64 : value!.type;
|
||||
const key = "indexed:fill:" + typeKey(arrType.elem) + ":" + typeKey(valueType) + ":" + writeUndefined;
|
||||
const writeNull = value?.type.kind === "nullT";
|
||||
// Unit literals have no parameter ABI. Pass a numeric placeholder and
|
||||
// synthesize the unit inside the helper's correctly tagged store.
|
||||
const discardValue = writeUndefined || writeNull;
|
||||
const valueType = discardValue ? F64 : value!.type;
|
||||
const key = "indexed:fill:" + typeKey(arrType.elem) + ":" + typeKey(valueType) + ":" + writeUndefined + ":" + writeNull;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (!name) {
|
||||
name = "%arr.fill." + lowerer.arrHofHelpers.size;
|
||||
@@ -51,7 +55,7 @@ export function lowerArrayFill(
|
||||
const i = varRef("i.0", F64, loc);
|
||||
const body: IrStmt[] = writeUndefined
|
||||
? [{ kind: "arraySetUndefined", arr: a, index: i, loc }]
|
||||
: [arrayValueStore(lowerer, a, i, varRef("v.0", valueType, loc), arrType.elem, loc)];
|
||||
: [arrayValueStore(lowerer, a, i, writeNull ? { kind: "unitLit", unit: "null", type: NULL_T, loc } : varRef("v.0", valueType, loc), arrType.elem, loc)];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [
|
||||
@@ -88,8 +92,9 @@ export function lowerArrayFill(
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const valueArg: IrExpr = value && writeUndefined
|
||||
? { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: value, loc }], result: numLit(0, loc), type: F64, loc }
|
||||
const valueArg: IrExpr = discardValue
|
||||
? value === null || value.kind === "unitLit" ? numLit(0, loc)
|
||||
: { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowerer.coerceToExpected(value, DYN), loc }], result: numLit(0, loc), type: F64, loc }
|
||||
: value ?? numLit(0, loc);
|
||||
return { kind: "call", callee: name, args: [receiver, valueArg, start, end], type: arrType, loc };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, VOID, isClassOwnEnumerableFieldName, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { dynUndefinedExpr, type Lowerer } from "./lowerer.js";
|
||||
import { findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
|
||||
/** Zero-argument notification methods can be replaced by own callbacks.
|
||||
* Other compiled signatures retain their method-replacement fence. */
|
||||
export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
if (!isClassOwnEnumerableFieldName(name) || name.startsWith("get:") || name.startsWith("set:")) return false;
|
||||
if (info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream || info.fields.has(name)) return false;
|
||||
const found = findMethodOn(lowerer, info, name);
|
||||
return !!found && found.sig.params.length === 0 && found.sig.ret.kind === "void" && !found.sig.abstract;
|
||||
}
|
||||
|
||||
function callbackBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
}
|
||||
|
||||
export function lowerClassCallbackAssign(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt | null {
|
||||
const access = expr.left;
|
||||
if (!ts.isPropertyAccessExpression(access) || access.questionDotToken) return null;
|
||||
const receiver = tryLowerExpression(lowerer, access.expression);
|
||||
if (!receiver) return null;
|
||||
const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
|
||||
if (!info || !isClassCallback(lowerer, info, access.name.text)) return null;
|
||||
const loc = locOf(expr);
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [callbackBag(lowerer, receiver, loc),
|
||||
{ kind: "strLit", value: access.name.text, type: STRING, loc }, lowerer.lowerExprExpecting(expr.right, DYN)], type: VOID, loc }, loc };
|
||||
}
|
||||
|
||||
/** Read the own property before arguments run, retaining callback identity
|
||||
* and the original native receiver for JavaScript's `this` binding. */
|
||||
export function classCallbackValue(lowerer: Lowerer, receiver: IrExpr, name: string, fallback: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
|
||||
const value = varRef(bag.id, DYN, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const result: IrExpr = { kind: "ternary", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [value, key], type: BOOL, loc },
|
||||
then: { kind: "dynKeyGet", value, key, type: DYN, loc }, else_: lowerer.coerceToExpected(fallback, DYN), type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: bag.id, init: callbackBag(lowerer, receiver, loc), loc }], result, type: DYN, loc };
|
||||
}
|
||||
|
||||
export function classCallbackCall(lowerer: Lowerer, receiver: IrExpr, name: string, args: IrExpr[], fallback: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
|
||||
const own = lowerer.declareHiddenLocal("%callbackOwn", BOOL);
|
||||
const callback = lowerer.declareHiddenLocal("%callback", DYN);
|
||||
const value = varRef(bag.id, DYN, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const call: IrExpr = { kind: "dynCall", callee: varRef(callback.id, DYN, loc), receiver: lowerer.coerceToExpected(receiver, DYN),
|
||||
calleeName: name, args, type: DYN, loc };
|
||||
const result: IrExpr = { kind: "ternary", cond: varRef(own.id, BOOL, loc), then: call,
|
||||
else_: { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: fallback, loc }], result: dynUndefinedExpr(loc), type: DYN, loc }, type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [
|
||||
{ kind: "varDecl", localId: bag.id, init: callbackBag(lowerer, receiver, loc), loc },
|
||||
{ kind: "varDecl", localId: own.id, init: { kind: "libCall", fn: "dyn.hasOwn", args: [value, key], type: BOOL, loc }, loc },
|
||||
{ kind: "varDecl", localId: callback.id, init: { kind: "dynKeyGet", value, key, type: DYN, loc }, loc },
|
||||
], result, type: DYN, loc };
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, type IrExpr, type IrStmt, isDynTypedRefType } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { isCompiledPrototypeMember } from "./class-prototypes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
|
||||
function literalName(name: ts.PropertyName): string | null {
|
||||
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
|
||||
}
|
||||
|
||||
function bagOnlyProperty(lowerer: Lowerer, owner: ClassInfo, name: string): boolean {
|
||||
return !owner.fields.has(name) && !isCompiledPrototypeMember(lowerer, owner, name) &&
|
||||
owner.subclasses.every((child) => bagOnlyProperty(lowerer, child, name));
|
||||
}
|
||||
|
||||
/** A named bag property needs no snapshot of unrelated native fields,
|
||||
* which may contain recursive or otherwise opaque values. */
|
||||
export function lowerClassDescriptorRead(lowerer: Lowerer, call: ts.CallExpression, target: IrExpr): IrExpr | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
const key = call.arguments[1]!;
|
||||
if (!info || info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream ||
|
||||
!ts.isStringLiteral(key) || !bagOnlyProperty(lowerer, info, key.text)) return null;
|
||||
const loc = locOf(call);
|
||||
const bag: IrExpr = { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(target, DYN)], type: DYN, loc };
|
||||
return { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args: [bag, lowerer.lowerExprExpecting(key, DYN)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Native layout fields cannot change descriptors. New named data properties
|
||||
* live in the instance's shared bag, preserving attributes and identity. */
|
||||
export function lowerClassDataDescriptor(lowerer: Lowerer, call: ts.CallExpression, member: string, target: IrExpr): IrExpr | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
if (!info || info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream) return null;
|
||||
const safeName = (owner: ClassInfo, name: string): boolean => bagOnlyProperty(lowerer, owner, name);
|
||||
const descriptor = (node: ts.Expression): boolean => ts.isObjectLiteralExpression(node) &&
|
||||
node.properties.every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
["value", "writable", "enumerable", "configurable"].includes(literalName(p.name) ?? ""));
|
||||
const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!;
|
||||
if (member === "defineProperty") {
|
||||
const key = call.arguments[1]!;
|
||||
if (!ts.isStringLiteral(key) || !safeName(info, key.text) || !descriptor(descriptors)) return null;
|
||||
} else {
|
||||
if (!ts.isObjectLiteralExpression(descriptors) || !descriptors.properties.every((p) => {
|
||||
if (!ts.isPropertyAssignment(p)) return false;
|
||||
const name = literalName(p.name);
|
||||
return name !== null && name !== "__proto__" && safeName(info, name) && descriptor(p.initializer);
|
||||
})) return null;
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.declareHiddenLocal("%descriptorReceiver", target.type);
|
||||
const desc = lowerer.declareHiddenLocal("%descriptors", DYN);
|
||||
const value = varRef(receiver.id, target.type, loc);
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
const helper = classPropertiesHelper(lowerer, loc);
|
||||
const bag: IrExpr = { kind: "call", callee: helper.name, args: [boxed], type: DYN, loc };
|
||||
const args = member === "defineProperty"
|
||||
? [bag, lowerer.lowerExprExpecting(call.arguments[1]!, DYN), varRef(desc.id, DYN, loc)]
|
||||
: [bag, varRef(desc.id, DYN, loc)];
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiver.id, init: target, loc },
|
||||
{ kind: "varDecl", localId: desc.id, init: lowerer.lowerExprExpecting(descriptors, DYN), loc },
|
||||
{ kind: "exprStmt", expr: { kind: "libCall", fn: member === "defineProperty" ? "dyn.defineProperty" : "dyn.defineProps", args, type: DYN, loc }, loc },
|
||||
];
|
||||
return { kind: "seqExpr", stmts, result: value, type: value.type, loc };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "vitest";
|
||||
import { DYN, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { ClassDynamicDispatch } from "./class-dynamic-dispatch.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
const loc = { file: "dispatch.ts", start: 0, end: 1 };
|
||||
const variable = (localId: string): IrExpr => ({ kind: "varRef", localId, type: DYN, loc });
|
||||
@@ -58,3 +59,40 @@ test("discovers dispatch sites added during a later reachability pass", () => {
|
||||
expect(dispatch.process(lowerer, [boxed, target])).toBe(true);
|
||||
expect(target.body[0]).toMatchObject({ kind: "exprStmt", expr: { kind: "call" } });
|
||||
});
|
||||
|
||||
test("known class property bags preserve one owned receiver without boxing", () => {
|
||||
const { lowerer, liftedFns } = context();
|
||||
const type = { kind: "object", className: "Widget" } as const;
|
||||
const info = { def: { name: "Widget", fields: [] }, fields: new Map(), base: null, subclasses: [] } as unknown as ClassInfo;
|
||||
lowerer.classes.set("Widget", info);
|
||||
const receiver: IrExpr = { kind: "call", callee: "makeWidget", args: [], type, loc };
|
||||
const target = fn("target", [statement({ kind: "call", callee: "%dyn.class.properties",
|
||||
args: [{ kind: "dynFrom", value: receiver, type: DYN, loc }], type: DYN, loc })]);
|
||||
const dispatch = new ClassDynamicDispatch();
|
||||
expect(dispatch.process(lowerer, [target])).toBe(true);
|
||||
expect(target.body).toEqual([statement({ kind: "call", callee: "%class.properties:Widget", args: [receiver], type: DYN, loc })]);
|
||||
const helper = liftedFns.find((item) => item.name === "%class.properties:Widget")!;
|
||||
expect(helper.params.map((param) => param.type)).toEqual([type]);
|
||||
expect(helper.body).toMatchObject([
|
||||
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: { kind: "fieldGet", className: "Widget" } },
|
||||
then: [{ kind: "fieldSet", obj: { kind: "varRef", type }, value: { kind: "dynObjLit" } }] },
|
||||
{ kind: "return", value: { kind: "fieldGet", className: "Widget" } },
|
||||
]);
|
||||
expect(dispatch.process(lowerer, [target, ...liftedFns])).toBe(false);
|
||||
// Prototype assignments can appear in a later reachable body. Both
|
||||
// lookup paths must then initialize the same inherited property bag.
|
||||
info.def.prototypeDataHelper = "%prototype.Widget";
|
||||
expect(dispatch.process(lowerer, [target, ...liftedFns])).toBe(true);
|
||||
expect(helper.body[0]).toMatchObject({ then: [{ value: { kind: "libCall", fn: "dyn.objCreate",
|
||||
args: [{ kind: "call", callee: "%prototype.Widget" }] } }] });
|
||||
expect(dispatch.process(lowerer, [target, ...liftedFns])).toBe(false);
|
||||
});
|
||||
|
||||
test("unknown receivers keep checked class property dispatch", () => {
|
||||
const { lowerer, boxed } = context();
|
||||
const expr: IrExpr = { kind: "call", callee: "%dyn.class.properties", args: [variable("value")], type: DYN, loc };
|
||||
const target = fn("target", [statement(expr)]);
|
||||
const body = target.body;
|
||||
new ClassDynamicDispatch().process(lowerer, [boxed, target]);
|
||||
expect(target.body).toBe(body);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
@@ -11,14 +11,22 @@ import { GeneratorDynamicDispatch } from "./generator-dynamic-dispatch.js";
|
||||
import { classMethodValue } from "./class-method-values.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { SYMBOL_T } from "../../ir/ir.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
|
||||
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
|
||||
interface Dispatch {
|
||||
source: Invoke;
|
||||
fn: IrFunction;
|
||||
callback: IrFunction;
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
interface PropertyReceiver {
|
||||
info: ClassInfo;
|
||||
capsule: ClassInfo;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface PropertyDispatch {
|
||||
name: string;
|
||||
write: boolean;
|
||||
@@ -26,11 +34,36 @@ interface PropertyDispatch {
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
export function classInstanceOf(lowerer: Lowerer, value: IrExpr, info: ClassInfo, loc: SrcLoc): IrExpr {
|
||||
const name = `%dyn.class.instanceof:${info.def.name}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
|
||||
name, params: [{ localId: "value", name: "value", type: DYN }], returnType: BOOL,
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }], loc,
|
||||
});
|
||||
return { kind: "call", callee: name, args: [value], type: BOOL, loc };
|
||||
}
|
||||
|
||||
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc): IrFunction {
|
||||
const name = "%dyn.class.properties";
|
||||
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
|
||||
if (existing) return existing;
|
||||
const helper: IrFunction = {
|
||||
name, params: [{ localId: "p.0", name: "value", type: DYN }], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: varRef("p.0", DYN, loc), loc }], loc,
|
||||
};
|
||||
lowerer.liftedFns.push(helper);
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Calls on native class capsules keep the instance's compiled methods. The
|
||||
* reachable-body fixed point discovers both the boxed classes and method
|
||||
* names before generating checked native dispatch; ordinary dyn receivers
|
||||
* continue through their existing runtime implementation. */
|
||||
export class ClassDynamicDispatch {
|
||||
constructor(private readonly asyncFree = false) {}
|
||||
|
||||
private readonly generatorDispatch = new GeneratorDynamicDispatch();
|
||||
private readonly boxed = new Set<string>();
|
||||
private readonly dispatches = new Map<string, Dispatch>();
|
||||
@@ -39,8 +72,10 @@ export class ClassDynamicDispatch {
|
||||
private propertyBag: IrFunction | null = null;
|
||||
private readonly errorStrings = new Map<string, IrFunction>();
|
||||
private readonly bagClasses = new Set<string>();
|
||||
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>>();
|
||||
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>[]>();
|
||||
private readonly typedPropertyBags = new Map<string, IrFunction>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
private readonly instanceTests = new Map<string, Set<string>>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
let changed = refreshErrorMethodDispatch(lowerer, functions);
|
||||
@@ -68,6 +103,9 @@ export class ClassDynamicDispatch {
|
||||
case "dynFrom":
|
||||
discover(expr.value.type);
|
||||
break;
|
||||
case "call":
|
||||
if (expr.callee === "%dyn.class.properties" && expr.args[0]?.kind === "dynFrom" && isDynTypedRefType(expr.args[0].value.type)) rewrite.add(fn);
|
||||
break;
|
||||
case "dynKeyGet":
|
||||
case "dynInvoke":
|
||||
rewrite.add(fn);
|
||||
@@ -81,26 +119,41 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
});
|
||||
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
|
||||
for (const fn of functions) {
|
||||
if (!fn.name.startsWith("%dyn.class.instanceof:")) continue;
|
||||
const target = fn.name.slice("%dyn.class.instanceof:".length);
|
||||
let checked = this.instanceTests.get(fn.name);
|
||||
if (!checked) { checked = new Set(); this.instanceTests.set(fn.name, checked); }
|
||||
for (const className of this.boxed) {
|
||||
if (checked.has(className)) continue;
|
||||
checked.add(className);
|
||||
const subtype = className === target || lowerer.isSubclassOf(className, target);
|
||||
if (!subtype && !lowerer.isSubclassOf(target, className)) continue;
|
||||
const loc = fn.loc;
|
||||
const value = varRef("value", DYN, loc);
|
||||
const type: IrType = { kind: "object", className };
|
||||
const result: IrExpr = subtype ? { kind: "boolLit", value: true, type: BOOL, loc }
|
||||
: { kind: "instanceOf", value: { kind: "dynCheck", value, type, loc }, className: target, type: BOOL, loc };
|
||||
fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value,
|
||||
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [{ kind: "return", value: result, loc }], else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!this.propertyBag) {
|
||||
const loc = functions[0]!.loc;
|
||||
this.propertyBag = {
|
||||
name: "%dyn.class.properties", params: [{ localId: "p.0", name: "value", type: DYN }], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: varRef("p.0", DYN, loc), loc }], loc,
|
||||
};
|
||||
this.propertyBag = classPropertiesHelper(lowerer, functions[0]!.loc);
|
||||
this.generated.add(this.propertyBag);
|
||||
lowerer.liftedFns.push(this.propertyBag);
|
||||
changed = true;
|
||||
}
|
||||
for (const className of this.boxed) {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
const existingInit = this.bagInitializers.get(className);
|
||||
if (existingInit && existingInit.value.kind === "dynObjLit" && hasClassPrototypeData(info)) {
|
||||
const prototype = classPrototypeData(lowerer, info, existingInit.loc, existingInit.obj);
|
||||
if (prototype) {
|
||||
existingInit.value = { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc: existingInit.loc };
|
||||
changed = true;
|
||||
for (const existingInit of this.bagInitializers.get(className) ?? []) {
|
||||
if (existingInit.value.kind === "dynObjLit" && hasClassPrototypeData(info)) {
|
||||
const prototype = classPrototypeData(lowerer, info, existingInit.loc, existingInit.obj);
|
||||
if (prototype) {
|
||||
existingInit.value = { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc: existingInit.loc };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.bagClasses.has(className)) continue;
|
||||
@@ -117,7 +170,7 @@ export class ClassDynamicDispatch {
|
||||
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
|
||||
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc,
|
||||
};
|
||||
this.bagInitializers.set(className, initialize);
|
||||
this.bagInitializers.set(className, [initialize]);
|
||||
this.propertyBag.body.unshift({
|
||||
kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
|
||||
then: [
|
||||
@@ -174,6 +227,16 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
return { kind: "call", callee: errorString.name, args: expr.args, type: STRING, loc: expr.loc };
|
||||
}
|
||||
if (expr.kind === "call" && expr.callee === this.propertyBag!.name) {
|
||||
const boxed = expr.args[0];
|
||||
if (boxed?.kind === "dynFrom" && isDynTypedRefType(boxed.value.type)) {
|
||||
const helper = this.typedPropertyBag(lowerer, boxed.value.type, expr.loc);
|
||||
if (helper) {
|
||||
changed = true;
|
||||
return { ...expr, callee: helper.name, args: [boxed.value] };
|
||||
}
|
||||
}
|
||||
}
|
||||
const computedRead = expr.kind === "dynKeyGet" && expr.key.kind !== "strLit" ? expr : null;
|
||||
const computedWrite = expr.kind === "libCall" && (expr.fn === "dyn.keySetComputed" || expr.fn === "dyn.keySet" && expr.args[1]?.kind !== "strLit") ? expr : null;
|
||||
const computedProbe = expr.kind === "libCall" && ["dyn.hasKeyComputed", "dyn.hasOwnComputed", "dyn.propertyIsEnumerableComputed"].includes(expr.fn) ? expr : null;
|
||||
@@ -271,6 +334,12 @@ export class ClassDynamicDispatch {
|
||||
let dispatch = this.dispatches.get(key);
|
||||
if (!dispatch) {
|
||||
const params = [invoke.recv, ...invoke.args].map((_, i) => ({ localId: `p.${i}`, name: `p${i}`, type: DYN }));
|
||||
params.splice(1, 0, { localId: "p.callback", name: "callback", type: DYN });
|
||||
const callback: IrFunction = {
|
||||
name: `%dyn.class.callback.${this.dispatches.size}`, params: [params[0]!], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "receiver", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: dynUndefinedExpr(expr.loc), loc: expr.loc }], loc: expr.loc,
|
||||
};
|
||||
const helper: IrFunction = {
|
||||
name: `%dyn.class.call.${this.dispatches.size}`, params, returnType: DYN,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
|
||||
@@ -289,13 +358,21 @@ export class ClassDynamicDispatch {
|
||||
}, loc },
|
||||
], else_: null, loc });
|
||||
}
|
||||
dispatch = { source: invoke, fn: helper, classes: new Set() };
|
||||
dispatch = { source: invoke, fn: helper, callback, classes: new Set() };
|
||||
this.dispatches.set(key, dispatch);
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
this.generated.add(callback);
|
||||
lowerer.liftedFns.push(helper, callback);
|
||||
changed = true;
|
||||
}
|
||||
return { kind: "call", callee: dispatch.fn.name, args: [invoke.recv, ...invoke.args], type: DYN, loc: expr.loc };
|
||||
// Resolve an own callback before argument effects can replace it.
|
||||
const local = { id: `%dispatch.receiver.${fn.locals.length}`, name: "receiver", type: DYN, mutable: false };
|
||||
fn.locals.push(local);
|
||||
const receiver = varRef(local.id, DYN, expr.loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: invoke.recv, loc: expr.loc }], result: {
|
||||
kind: "call", callee: dispatch.fn.name, args: [receiver,
|
||||
{ kind: "call", callee: dispatch.callback.name, args: [receiver], type: DYN, loc: expr.loc }, ...invoke.args], type: DYN, loc: expr.loc,
|
||||
}, type: DYN, loc: expr.loc };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -306,6 +383,13 @@ export class ClassDynamicDispatch {
|
||||
const loc = dispatch.source.loc;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const receiver = varRef("p.0", DYN, loc);
|
||||
if (isClassCallback(lowerer, info, dispatch.source.method)) {
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [receiver], type: DYN, loc };
|
||||
dispatch.callback.body.unshift({ kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: [{ kind: "return", value: { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args: [bag,
|
||||
lowerer.coerceToExpected({ kind: "strLit", value: dispatch.source.method, type: STRING, loc }, DYN)], type: DYN, loc }, loc }], else_: null, loc });
|
||||
}
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
try {
|
||||
@@ -325,16 +409,19 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
for (const dispatch of this.properties.values()) {
|
||||
for (const info of this.propertyCandidates(lowerer, dispatch.name)) {
|
||||
if (dispatch.classes.has(info.def.name)) continue;
|
||||
dispatch.classes.add(info.def.name);
|
||||
for (const plan of this.propertyReceivers(lowerer)) {
|
||||
const { info } = plan;
|
||||
if (!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
!info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name)) continue;
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const receiver = varRef("p.0", DYN, loc);
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
try {
|
||||
branch = this.propertyBody(lowerer, dispatch, info, { kind: "dynCheck", value: receiver, type, loc });
|
||||
branch = this.propertyBody(lowerer, dispatch, info, receiver);
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError) || !info.decl) throw error;
|
||||
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
|
||||
@@ -342,23 +429,18 @@ export class ClassDynamicDispatch {
|
||||
branch = [fence];
|
||||
}
|
||||
dispatch.fn.body.unshift({
|
||||
kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: branch, else_: null, loc,
|
||||
kind: "if", cond: condition, then: branch, else_: null, loc,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (const dispatch of this.computed.values()) {
|
||||
for (const className of this.boxed) {
|
||||
if (dispatch.classes.has(className)) continue;
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
dispatch.classes.add(className);
|
||||
for (const plan of this.propertyReceivers(lowerer)) {
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
const { info } = plan;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const type: IrType = { kind: "object", className };
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
const memberBody = (name: string): IrStmt[] => dispatch.probe
|
||||
? [{ kind: "return", value: { kind: "boolLit", value: info.fields.has(name) || dispatch.probe === "in", type: BOOL, loc }, loc }]
|
||||
: this.propertyBody(lowerer, { ...dispatch, name }, info, receiver);
|
||||
@@ -395,7 +477,7 @@ export class ClassDynamicDispatch {
|
||||
branches.push({ kind: "if", cond: { kind: "dynScalarEq", left: varRef("p.key", DYN, loc), right: symbol, type: BOOL, loc }, then, else_: null, loc });
|
||||
}
|
||||
if (branches.length) {
|
||||
dispatch.fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [
|
||||
dispatch.fn.body.unshift({ kind: "if", cond: condition, then: [
|
||||
{ kind: "if", cond: { kind: "dynTest", test: "symbol", value: varRef("p.key", DYN, loc), type: BOOL, loc }, then: branches, else_: null, loc },
|
||||
], else_: null, loc });
|
||||
dispatch.branchIndex++;
|
||||
@@ -426,15 +508,43 @@ export class ClassDynamicDispatch {
|
||||
kind: "strEq", left: varRef(dispatch.keyLocal, STRING, loc), right: { kind: "strLit", value: name, type: STRING, loc }, negated: false, type: BOOL, loc,
|
||||
}, then: body, else_: null, loc });
|
||||
}
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: branch, else_: null, loc });
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: condition, then: branch, else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** A known native class can access its shared bag directly. Keep the
|
||||
* receiver as an owned parameter so calls and temporary instances retain
|
||||
* their ordinary evaluation and lifetime rules without a boxed capsule. */
|
||||
private typedPropertyBag(lowerer: Lowerer, type: Extract<IrType, { kind: "object" }>, loc: SrcLoc): IrFunction | null {
|
||||
const existing = this.typedPropertyBags.get(type.className);
|
||||
if (existing) return existing;
|
||||
const initializers = this.bagInitializers.get(type.className);
|
||||
if (!initializers) return null;
|
||||
const receiver = varRef("p.0", type, loc);
|
||||
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className: type.className, field: PROPERTY_BAG, type: DYN, loc };
|
||||
const info = lowerer.classes.get(type.className)!;
|
||||
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc, receiver) : null;
|
||||
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = { ...initializers[0]!, obj: receiver, value: prototype
|
||||
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
|
||||
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc };
|
||||
const helper: IrFunction = {
|
||||
name: `%class.properties:${type.className}`, params: [{ localId: "p.0", name: "value", type }], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "value", type, mutable: false }], loc,
|
||||
body: [
|
||||
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, then: [initialize], else_: null, loc },
|
||||
{ kind: "return", value: bag, loc },
|
||||
],
|
||||
};
|
||||
initializers.push(initialize);
|
||||
this.typedPropertyBags.set(type.className, helper);
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** The hidden bag is part of the native object layout, so every capsule
|
||||
* shares it and normal class tracing/disposal owns its values. Insert it
|
||||
* at the same prefix offset throughout a hierarchy, including classes
|
||||
@@ -454,14 +564,34 @@ export class ClassDynamicDispatch {
|
||||
add(root);
|
||||
}
|
||||
|
||||
private propertyCandidates(lowerer: Lowerer, name: string): ClassInfo[] {
|
||||
if (!isClassOwnEnumerableFieldName(name)) return [];
|
||||
return [...this.boxed].flatMap((className) => {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) return [];
|
||||
return info.fields.has(name) || findMethodOn(lowerer, info, `get:${name}`) || findMethodOn(lowerer, info, `set:${name}`) ||
|
||||
findMethodOn(lowerer, info, name) || findGenericMethodOn(lowerer, info, name) ? [info] : [];
|
||||
});
|
||||
/** A capsule retains its static type, while its object may be a subclass.
|
||||
* Preorder insertion lets later branches test the most derived layout first. */
|
||||
private propertyReceivers(lowerer: Lowerer): PropertyReceiver[] {
|
||||
const plans: PropertyReceiver[] = [];
|
||||
for (const name of this.boxed) {
|
||||
const capsule = lowerer.classes.get(name);
|
||||
if (!capsule || capsule.builtinEmitter || capsule.builtinStream || capsule.builtinError) continue;
|
||||
const visit = (info: ClassInfo): void => {
|
||||
plans.push({ info, capsule, key: JSON.stringify([name, info.def.name]) });
|
||||
for (const child of info.subclasses) visit(child);
|
||||
};
|
||||
visit(capsule);
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
private propertyReceiver(plan: PropertyReceiver, loc: SrcLoc): { receiver: IrExpr; condition: IrExpr } {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const type: IrType = { kind: "object", className: plan.capsule.def.name };
|
||||
const checked: IrExpr = { kind: "dynCheck", value, type, loc };
|
||||
const matches: IrExpr = { kind: "libCall", fn: "dyn.typedRefIs", args: [value,
|
||||
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc };
|
||||
if (plan.info === plan.capsule) return { receiver: checked, condition: matches };
|
||||
return {
|
||||
receiver: { kind: "downcast", value: checked, type: { kind: "object", className: plan.info.def.name }, loc },
|
||||
condition: { kind: "ternary", cond: matches, then: { kind: "instanceOf", value: checked,
|
||||
className: plan.info.def.name, type: BOOL, loc }, else_: { kind: "boolLit", value: false, type: BOOL, loc }, type: BOOL, loc },
|
||||
};
|
||||
}
|
||||
|
||||
private propertyBody(lowerer: Lowerer, dispatch: PropertyDispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
|
||||
@@ -478,6 +608,14 @@ export class ClassDynamicDispatch {
|
||||
return [{ kind: "return", value: errorToStringMethod(lowerer, receiver), loc }];
|
||||
}
|
||||
if (!field && !accessor && info.decl) {
|
||||
const declared = findMethodOn(lowerer, info, name);
|
||||
// Untyped dispatch considers native classes that may never reach
|
||||
// this lookup. Libraries cannot expose fibers or promises through
|
||||
// those speculative branches. Direct typed references still reach
|
||||
// the ordinary graph-wide async_free diagnostic.
|
||||
if (this.asyncFree && (declared?.sig.gen || declared?.sig.async)) {
|
||||
return [{ kind: "runtimeFence", code: "SC4005", message: `reading async or generator method '${name}' through an untyped library lookup is not supported`, loc }];
|
||||
}
|
||||
const generic = findGenericMethodOn(lowerer, info, name);
|
||||
// A computed lookup has branches for every visible name. Generic
|
||||
// methods need a concrete specialization before they can escape;
|
||||
@@ -538,6 +676,9 @@ export class ClassDynamicDispatch {
|
||||
const { method, loc } = dispatch.source;
|
||||
const fence = (): IrStmt[] => [{ kind: "runtimeFence", code: "SC2020", message: `calling '${method}' on this native class through an untyped value is not supported yet`, loc }];
|
||||
const methodInfo = findMethodOn(lowerer, info, method);
|
||||
if (this.asyncFree && (methodInfo?.sig.gen || methodInfo?.sig.async)) {
|
||||
return [{ kind: "runtimeFence", code: "SC4005", message: `calling async or generator method '${method}' through an untyped library value is not supported`, loc }];
|
||||
}
|
||||
if (methodInfo?.declarer.builtinError && method === "toString") {
|
||||
return [{ kind: "return", value: { kind: "dynFrom", value: errorToStringCall(lowerer, receiver), type: DYN, loc }, loc }];
|
||||
}
|
||||
@@ -605,6 +746,14 @@ export class ClassDynamicDispatch {
|
||||
if (virtual) lowerer.noteVirtualEdge(owner, method);
|
||||
else lowerer.noteEdge(callee);
|
||||
}
|
||||
if (isClassCallback(lowerer, info, method)) {
|
||||
const descriptor = varRef("p.callback", DYN, loc);
|
||||
body.unshift({ kind: "if", cond: { kind: "dynTest", test: "undefined", negated: true, value: descriptor, type: BOOL, loc }, then: [
|
||||
{ kind: "return", value: { kind: "dynCall", callee: { kind: "dynKeyGet", value: descriptor,
|
||||
key: { kind: "strLit", value: "value", type: STRING, loc }, type: DYN, loc }, receiver: varRef("p.0", DYN, loc),
|
||||
args: incoming, calleeName: dispatch.source.calleeName, type: DYN, loc }, loc },
|
||||
], else_: null, loc });
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, VOID, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { classValueRef, upcastTo, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
|
||||
/** Select the instance's concrete class before evaluating constructor
|
||||
* arguments. Each branch completes arguments against its own constructor,
|
||||
* including subclasses with different defaults or parameter counts. */
|
||||
export function lowerInstanceConstructorNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
let access = expr.expression;
|
||||
while (ts.isParenthesizedExpression(access)) access = access.expression;
|
||||
if (!ts.isPropertyAccessExpression(access) || access.name.text !== "constructor" || access.questionDotToken) return null;
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
if (receiver.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(receiver.type.className);
|
||||
if (!info || info.def.runtime) return null;
|
||||
const loc = locOf(expr);
|
||||
const local = lowerer.declareHiddenLocal("%constructorReceiver", receiver.type);
|
||||
const value = varRef(local.id, receiver.type, loc);
|
||||
const construct = (current: ClassInfo): IrExpr => {
|
||||
if (current.fields.has("constructor") || current.generic || current.localClass || current.classDecorators) {
|
||||
lowerer.unsupported("SC1090", expr, "instance constructors on generic, local, decorated, or constructor-shadowing classes");
|
||||
}
|
||||
classValueRef(lowerer, current, expr);
|
||||
const result: IrExpr = {
|
||||
kind: "new", className: current.def.name,
|
||||
args: lowerer.completeArgs(expr.arguments ?? [], current.ctorParams, loc, expr),
|
||||
type: { kind: "object", className: current.def.name }, loc,
|
||||
};
|
||||
return upcastTo(lowerer, result, info.def.name);
|
||||
};
|
||||
const visit = (current: ClassInfo): IrExpr => {
|
||||
let result = construct(current);
|
||||
for (const child of current.subclasses) result = {
|
||||
kind: "ternary", cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc },
|
||||
then: visit(child), else_: result, type: receiver.type, loc,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
const bag: IrExpr = { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(value, DYN)], type: DYN, loc };
|
||||
const check = "%class.constructor.check";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === check)) lowerer.liftedFns.push({
|
||||
name: check, params: [{ localId: "bag", name: "bag", type: DYN }], returnType: VOID,
|
||||
locals: [{ id: "bag", name: "bag", type: DYN, mutable: false }], loc,
|
||||
body: [{ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [varRef("bag", DYN, loc),
|
||||
{ kind: "strLit", value: "constructor", type: STRING, loc }], type: BOOL, loc }, then: [
|
||||
{ kind: "runtimeFence", code: "SC1090", message: "construction through a replaced instance constructor is not supported yet", loc },
|
||||
], else_: null, loc }, { kind: "return", value: null, loc }],
|
||||
});
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: local.id, init: receiver, loc },
|
||||
{ kind: "exprStmt", expr: { kind: "call", callee: check, args: [bag], type: VOID, loc }, loc },
|
||||
];
|
||||
return { kind: "seqExpr", stmts, result: visit(info), type: receiver.type, loc };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { Lowerer } from "./lowerer.js";
|
||||
import { findGenericMethodOn, findMethodOn, genericOverrideBelow, type ClassInfo } from "./lower-classes.js";
|
||||
import { funcTypeFromParamShapes, implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
|
||||
import { errorToStringMethod } from "./error-methods.js";
|
||||
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
|
||||
|
||||
/** A method value retains its declaration's identity, not the receiver from
|
||||
* extraction. Its native thunk validates the receiver supplied at call time. */
|
||||
@@ -18,10 +19,19 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
|
||||
const value = methodValue(lowerer, expr, info);
|
||||
if (!value) return null;
|
||||
const loc = locOf(expr);
|
||||
const callback = isClassCallback(lowerer, info, method);
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const local = callback ? lowerer.declareHiddenLocal("%callbackReceiver", receiver.type) : null;
|
||||
const reference = local ? varRef(local.id, receiver.type, loc) : receiver;
|
||||
const finish = (result: IrExpr): IrExpr => {
|
||||
if (!local) return result;
|
||||
const selected = classCallbackValue(lowerer, reference, method, result, loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], result: selected, type: selected.type, loc };
|
||||
};
|
||||
const overrides = [...lowerer.classes.values()].filter((candidate) =>
|
||||
candidate !== info && lowerer.isSubclassOf(candidate.def.name, info.def.name) && candidate.methods.has(method));
|
||||
if (overrides.length === 0) {
|
||||
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.expression), loc }], result: value, type: value.type, loc };
|
||||
return finish({ kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: reference, loc }], result: value, type: value.type, loc });
|
||||
}
|
||||
// Select the declaration when extracting the value. Calling the value
|
||||
// later must not redispatch the method name on a different receiver.
|
||||
@@ -42,7 +52,7 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
|
||||
lowerer.liftedFns.push({ name, params: [{ localId: "this.0", name: "this", type: receiverType }], returnType: value.type,
|
||||
locals: [{ id: "this.0", name: "this", type: receiverType, mutable: false }], body, loc });
|
||||
}
|
||||
return { kind: "call", callee: name, args: [lowerer.lowerExpr(expr.expression)], type: value.type, loc };
|
||||
return finish({ kind: "call", callee: name, args: [reference], type: value.type, loc });
|
||||
}
|
||||
|
||||
function methodValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression, info: ClassInfo): IrExpr | null {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrFunction, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { findStaticOn, findGenericStaticOn, type ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
const constructorMembers = new Set([
|
||||
"name", "length", "prototype", "__proto__", "constructor", "call", "apply", "bind",
|
||||
"arguments", "caller", "toString", "toLocaleString", "valueOf", "hasOwnProperty",
|
||||
"isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__defineSetter__",
|
||||
"__lookupGetter__", "__lookupSetter__",
|
||||
]);
|
||||
|
||||
/** Expando data on an exact JS constructor. Declared statics and Function
|
||||
* members retain their existing lowering; this bag cannot replace them. */
|
||||
export function classStaticDataFor(lowerer: Lowerer, info: ClassInfo, name: string, loc: SrcLoc): IrExpr | null {
|
||||
if (!info.decl || !isJsSourceFile(info.decl.getSourceFile()) || name.startsWith("#") || constructorMembers.has(name)) return null;
|
||||
if (findStaticOn(lowerer, info, name) || findGenericStaticOn(lowerer, info, name)) return null;
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.decl?.members.some((m) => m.name &&
|
||||
ts.canHaveModifiers(m) && ts.getModifiers(m)?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword) &&
|
||||
(ts.isComputedPropertyName(m.name) || ((ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) && m.name.text === name)))) return null;
|
||||
}
|
||||
return classStaticData(lowerer, info, loc);
|
||||
}
|
||||
|
||||
function classStaticData(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrExpr | null {
|
||||
if (info.localClass || info.mixinInstance || info.generic || info.genericInstance || info.classDecorators || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null;
|
||||
if (info.staticDataHelper === undefined) {
|
||||
const base = info.base ? classStaticData(lowerer, info.base, loc) : null;
|
||||
if (info.base && base === null) return null;
|
||||
const name = `%static.data.${info.def.name}`;
|
||||
const globalId = `%g.${name}`;
|
||||
const readyId = `${globalId}.ready`;
|
||||
info.staticDataHelper = name;
|
||||
lowerer.globalsList.push({ id: globalId, name: `${info.def.name}.staticData`, type: DYN, mutable: true });
|
||||
lowerer.globalsList.push({ id: readyId, name: `${info.def.name}.staticDataReady`, type: BOOL, mutable: true });
|
||||
const helper: IrFunction = {
|
||||
name, params: [], returnType: DYN, locals: [], loc,
|
||||
body: [
|
||||
{ kind: "if", cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, then: [
|
||||
{ kind: "assign", localId: globalId, value: base
|
||||
? { kind: "libCall", fn: "dyn.objCreate", args: [base], type: DYN, loc }
|
||||
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc },
|
||||
{ kind: "assign", localId: readyId, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
], else_: null, loc },
|
||||
{ kind: "return", value: varRef(globalId, DYN, loc), loc },
|
||||
],
|
||||
};
|
||||
lowerer.liftedFns.push(helper);
|
||||
}
|
||||
return { kind: "call", callee: info.staticDataHelper, args: [], type: DYN, loc };
|
||||
}
|
||||
@@ -193,23 +193,14 @@ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol:
|
||||
return { kind: "bytesNew", source: count, type, loc };
|
||||
}
|
||||
const src = lowerer.lowerExpr(argNode);
|
||||
if (src.type.kind === "union" && lowerer.armTag(src.type.unionId, UNDEFINED_T) >= 0) {
|
||||
const present = lowerer.stripUndefinedArm(src.type);
|
||||
if (present.kind === "array" && present.elem.kind === "f64") {
|
||||
const undefTag = lowerer.armTag(src.type.unionId, UNDEFINED_T);
|
||||
const presentTag = lowerer.armTag(src.type.unionId, present);
|
||||
if (undefTag >= 0 && presentTag >= 0) {
|
||||
const empty: IrExpr = { kind: "arrayLit", elems: [], type: present, loc };
|
||||
const source: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId: src.type.unionId, tag: undefTag, negated: false, value: src, type: BOOL, loc },
|
||||
then: empty,
|
||||
else_: { kind: "unionNarrow", unionId: src.type.unionId, tag: presentTag, value: src, type: present, loc },
|
||||
type: present,
|
||||
loc,
|
||||
};
|
||||
return { kind: "bytesNew", source, type, loc };
|
||||
}
|
||||
if (src.type.kind === "union") {
|
||||
const arms = lowerer.unions.get(src.type.unionId)?.arms;
|
||||
if (arms?.every((arm) => arm.kind === "f64" || arm.kind === "bytes" ||
|
||||
typeEquals(arm, UNDEFINED_T) || (arm.kind === "array" && arm.elem.kind === "f64"))) {
|
||||
// The checked constructor dispatch preserves each arm's copy or
|
||||
// length semantics, including undefined -> empty, and evaluates
|
||||
// an effectful source expression exactly once.
|
||||
return { kind: "bytesNew", source: lowerer.coerceInto(argNode, src, DYN), type, loc };
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -302,7 +302,9 @@ export function lowerDynObjectLiteral(
|
||||
const lowerValue = (): IrExpr =>
|
||||
ts.isMethodDeclaration(prop)
|
||||
? (isJsSourceFile(expr.getSourceFile()) ? lowerer.lowerLambda(prop) : (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop)))
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
|
||||
? lowerer.lowerExprExpecting(valueExpr, DYN)
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
raw =
|
||||
fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
|
||||
lowerValue();
|
||||
@@ -325,6 +327,11 @@ export function lowerDynObjectLiteral(
|
||||
let v = boxValue
|
||||
? boxValue(valueExpr as ts.Expression, raw)
|
||||
: lowerer.coerceToExpected(raw, DYN);
|
||||
// A JS object's array-valued properties retain the same source array.
|
||||
// Repeated references must compare equal and mutations remain shared.
|
||||
if (!boxValue && isJsSourceFile(prop.getSourceFile()) && v.kind === "dynFrom" && v.value.type.kind === "array") {
|
||||
v = { ...v, liveRef: true };
|
||||
}
|
||||
if (v.type.kind !== "dyn") {
|
||||
const convDiagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
|
||||
@@ -2303,6 +2303,12 @@ function lowerFsSyncBufferWindow(
|
||||
}
|
||||
if (bi.module === "fs" && (bi.member === "writeFileSync" || bi.member === "appendFileSync") && expr.arguments.length === 2) {
|
||||
const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!));
|
||||
if (dataIr?.kind === "union") {
|
||||
const arms = lowerer.unions.get(dataIr.unionId)?.arms;
|
||||
if (arms && arms.every((arm) => arm.kind === "string" || (arm.kind === "bytes" && arm.elem === "u8"))) {
|
||||
return lowerStringOrBytesWrite(lowerer, expr, dataIr, arms, bi.member === "appendFileSync");
|
||||
}
|
||||
}
|
||||
if (dataIr?.kind === "bytes") {
|
||||
if (dataIr.elem !== "u8") {
|
||||
lowerer.noLowering(
|
||||
@@ -4995,17 +5001,49 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
|
||||
);
|
||||
}
|
||||
const node: IrExpr = { kind: "jsonStringify", value, type: STRING, loc };
|
||||
if (indent !== "") {
|
||||
// The compile-time-resolved indent rides as an extra property (the
|
||||
// node shape in ir/ir.ts is unchanged); the backend re-indents
|
||||
// the compact serializer output with Node's gap algorithm.
|
||||
(node as { indent?: string }).indent = indent;
|
||||
}
|
||||
if (indent !== "") node.indent = indent;
|
||||
return node;
|
||||
}
|
||||
return null; // unknown members are tsc errors before lowering
|
||||
}
|
||||
|
||||
/** A valid string/byte overload must select its runtime entry from the live
|
||||
* union tag. Coercing the whole argument to the string overload loses bytes. */
|
||||
function lowerStringOrBytesWrite(lowerer: Lowerer, call: ts.CallExpression,
|
||||
dataType: Extract<IrType, { kind: "union" }>, arms: readonly IrType[], append: boolean): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`;
|
||||
let helper = lowerer.widthHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%fs.writeData.${lowerer.widthHelpers.size}`;
|
||||
lowerer.widthHelpers.set(key, helper);
|
||||
const body: IrStmt[] = [];
|
||||
for (let tag = 0; tag < arms.length; tag++) {
|
||||
const arm = arms[tag]!;
|
||||
const fn: IrLibFn = arm.kind === "bytes"
|
||||
? append ? "fs.appendFileSyncBytes" : "fs.writeFileSyncBytes"
|
||||
: append ? "fs.appendFileSync" : "fs.writeFileSync";
|
||||
body.push({ kind: "if", loc,
|
||||
cond: { kind: "unionIsTag", unionId: dataType.unionId, tag, negated: false,
|
||||
value: varRef("data.0", dataType, loc), type: BOOL, loc },
|
||||
then: [
|
||||
{ kind: "exprStmt", expr: { kind: "libCall", fn, args: [varRef("path.0", STRING, loc),
|
||||
{ kind: "unionNarrow", unionId: dataType.unionId, tag, value: varRef("data.0", dataType, loc), type: arm, loc }], type: VOID, loc }, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
], else_: null,
|
||||
});
|
||||
}
|
||||
body.push({ kind: "return", value: null, loc });
|
||||
lowerer.liftedFns.push({ name: helper, returnType: VOID, loc,
|
||||
params: [{ localId: "path.0", name: "path", type: STRING }, { localId: "data.0", name: "data", type: dataType }],
|
||||
locals: [{ id: "path.0", name: "path", type: STRING, mutable: false }, { id: "data.0", name: "data", type: dataType, mutable: false }],
|
||||
body,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [lowerer.lowerExprExpecting(call.arguments[0]!, STRING),
|
||||
lowerer.lowerExprExpecting(call.arguments[1]!, dataType)], type: VOID, loc };
|
||||
}
|
||||
|
||||
function lowerOptionalStringifyRoot(lowerer: Lowerer, value: IrExpr, indent: string, loc: SrcLoc): IrExpr | null {
|
||||
const tags = optionalStringTags(lowerer, value.type);
|
||||
if (!tags || value.type.kind !== "union") return null;
|
||||
@@ -5022,7 +5060,7 @@ function lowerOptionalStringifyRoot(lowerer: Lowerer, value: IrExpr, indent: str
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (indent !== "") (serialized as { indent?: string }).indent = indent;
|
||||
if (indent !== "") serialized.indent = indent;
|
||||
const missing = lowerer.wrappedUndefined(resultT, loc);
|
||||
if (!missing) throw new InternalCompilerError("optional JSON.stringify result needs an undefined arm");
|
||||
lowerer.liftedFns.push({
|
||||
@@ -6137,7 +6175,11 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
|
||||
lowerer.noLowering(`${receiverType.kind === "cryptoHash" ? "Hash" : "Hmac"}.update with ${call.arguments.length} arguments`, call, "update(stringOrBuffer[, inputEncoding]) is supported");
|
||||
}
|
||||
const dataNode = call.arguments[0]!;
|
||||
const data = lowerer.lowerExpr(dataNode);
|
||||
const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
|
||||
// Indexed reads can retain optional storage after a nullish fallback
|
||||
// or narrowing. Use the proven argument type through a checked coercion.
|
||||
const data = dataType?.kind === "string" || dataType?.kind === "bytes"
|
||||
? lowerer.lowerExprExpecting(dataNode, dataType) : lowerer.lowerExpr(dataNode);
|
||||
const prefix = receiverType.kind === "cryptoHash" ? "crypto.hashUpdate" : "crypto.hmacUpdate";
|
||||
if (data.type.kind === "bytes" && data.type.elem === "u8") {
|
||||
if (call.arguments.length !== 1) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { objectEnumerationReceiver } from "./object-enumeration-receiver.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Call lowering: the lowerCall dispatch chain, parameter-shape analysis and
|
||||
* argument completion (optional/default/rest, explicit-undefined ≡ omission),
|
||||
@@ -29,7 +30,8 @@ import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, st
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
|
||||
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js";
|
||||
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, returnsOnlyThis, type ClassInfo } from "./lower-classes.js";
|
||||
import { classCallbackCall, isClassCallback } from "./class-callbacks.js";
|
||||
import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js";
|
||||
import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
|
||||
import { STREAM_API_MEMBERS, lowerStreamMethodCall, lowerStreamModuleCall, lowerStreamStaticCall, streamSidesOf } from "./lower-stream.js";
|
||||
@@ -42,6 +44,8 @@ import { rejectStaticThis } from "./static-this.js";
|
||||
import { fenceNodeModuleMutationCall, lowerRequireCacheKeys } from "./lower-node-module.js";
|
||||
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument, positionNumber } from "./optional-arguments.js";
|
||||
import { fenceSymbolFieldCopy } from "./symbol-fields.js";
|
||||
import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
|
||||
export { bodyReadsArguments };
|
||||
|
||||
@@ -183,9 +187,12 @@ export interface GenericFnInfo {
|
||||
* Instance methods take `this` (object:<declarer>) as param 0 and lower
|
||||
* under the declarer's instantiation bindings (generic-class receivers)
|
||||
* MERGED with the method instantiation's own; statics lower as plain
|
||||
* module functions with the static-method this/super fence. Absent for
|
||||
* module functions with an exact receiver or the this/super fence. Absent for
|
||||
* top-level functions and object-literal methods. */
|
||||
member?: { cls: ClassInfo; kind: "method" | "static" };
|
||||
/** Direct static calls specialize lexical this for the proven receiver. */
|
||||
staticReceiver?: ClassInfo;
|
||||
receiverSpecializations?: Map<string, GenericFnInfo>;
|
||||
/** Object-literal generic methods (`{ m<T>(x: T) {...} }` and generic
|
||||
* arrow/function-expression properties): lowered as plain module
|
||||
* functions — `this` inside is fenced (rejectThisInObjectMethod) and the
|
||||
@@ -1749,19 +1756,24 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
lowerer.fnStack.push(fnCtx);
|
||||
lowerer.localClassInstantiations.push({ owner: decl, name: inst.name });
|
||||
try {
|
||||
// STATIC generic methods: `this`/`super` name the RECEIVER class (a
|
||||
// dynamic value) — the lowerStaticMethod fence, applied here because
|
||||
// generic statics have no non-generic lowering pass. Arrow functions
|
||||
// are transparent (they inherit the method's `this`); this-binding
|
||||
// function forms are opaque.
|
||||
// Static specializations bind lexical this only when the caller
|
||||
// proved its exact receiver. Other instances retain the fence;
|
||||
// super remains unsupported. Arrows inherit the lexical receiver.
|
||||
if (info.member?.kind === "static" && decl.body) {
|
||||
rejectStaticThis(
|
||||
lowerer,
|
||||
decl.body,
|
||||
(keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`,
|
||||
false, info.staticReceiver !== undefined,
|
||||
);
|
||||
}
|
||||
const params: IrParam[] = [];
|
||||
const receiverInit: IrStmt[] = [];
|
||||
if (info.staticReceiver) {
|
||||
const value = lowerer.classValueRef(info.staticReceiver, decl);
|
||||
const local = lowerer.declareThis(value.type);
|
||||
receiverInit.push({ kind: "varDecl", localId: local.id, init: value, loc: locOf(decl) });
|
||||
}
|
||||
if (cls && info.member!.kind === "method") {
|
||||
// Instance methods take `this` as param 0, exactly like plain
|
||||
// `%C.method` functions (lowerClassMethodMemberInner).
|
||||
@@ -1773,7 +1785,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
// threaded — a default mentioning T resolves like any body expression.
|
||||
const declared = lowerer.declareParams(decl.parameters, inst.params);
|
||||
params.push(...declared.params);
|
||||
const body = [...declared.prologue];
|
||||
const body = [...receiverInit, ...declared.prologue];
|
||||
const bodyBlock = blockBodyOf(decl);
|
||||
if (bodyBlock) {
|
||||
body.push(...lowerer.lowerStmts(bodyBlock.statements));
|
||||
@@ -1882,21 +1894,22 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
// The wrap pass: settle every recorded return onto `final`, in place.
|
||||
for (const e of infer.entries) {
|
||||
if (e.stmt.kind !== "return") continue;
|
||||
const st = e.stmt as IrStmt & { kind: "return"; value: IrExpr | null };
|
||||
// Write through the recorded statement. A narrowed local can acquire a
|
||||
// different record layout and copy it in the native compiler.
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
if (st.value === null || st.value === undefined) {
|
||||
if (final.kind === "dyn") st.value = dynUndefinedExpr(st.loc);
|
||||
if (e.stmt.value === null || e.stmt.value === undefined) {
|
||||
if (final.kind === "dyn") e.stmt.value = dynUndefinedExpr(e.stmt.loc);
|
||||
else if (final.kind === "union") {
|
||||
const wrapped = lowerer.wrappedUndefined(final, st.loc);
|
||||
const wrapped = lowerer.wrappedUndefined(final, e.stmt.loc);
|
||||
if (!wrapped) {
|
||||
lowerer.unsupported("SC1090", e.node ?? decl, `bare 'return' in a function whose inferred return type is '${lowerer.fmt(final)}'`);
|
||||
}
|
||||
st.value = wrapped;
|
||||
e.stmt.value = wrapped;
|
||||
}
|
||||
// void final: bare return stands as-is
|
||||
} else if (!typeEquals(st.value.type, final)) {
|
||||
st.value = lowerer.coerceInto(e.node ?? decl, st.value, final);
|
||||
} else if (!typeEquals(e.stmt.value.type, final)) {
|
||||
e.stmt.value = lowerer.coerceInto(e.node ?? decl, e.stmt.value, final);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof PoisonError)) throw err;
|
||||
@@ -1908,7 +1921,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
if (ice.length > 0) lowerer.diags.push(...ice);
|
||||
lowerer.runtimeFences.push(...captured.filter((d) => d.code !== "SC9001"));
|
||||
const first = captured.find((d) => d.code !== "SC9001");
|
||||
const mutable = st as unknown as Record<string, unknown>;
|
||||
const mutable = e.stmt as unknown as Record<string, unknown>;
|
||||
delete mutable["value"];
|
||||
mutable["kind"] = "runtimeFence";
|
||||
mutable["code"] = first?.code ?? "SC1090";
|
||||
@@ -2172,11 +2185,14 @@ function runtimeOptionalHofGenericBinding(
|
||||
|
||||
/** The implicit-type-parameter slots of a JS function-like: parallel to
|
||||
* decl.parameters, the param SYMBOL where the slot is a bindable
|
||||
* implicit-any param (identifier-named, no annotation/JSDoc type, not
|
||||
* rest/optional/defaulted, never written), null elsewhere. Null overall
|
||||
* when nothing qualifies — the declaration keeps today's path. */
|
||||
* implicit param (identifier-named, broad inferred/JSDoc type, not
|
||||
* rest/optional/defaulted, never written), null elsewhere. Package JS
|
||||
* methods may also specialize class parameters: JSDoc names a nominal
|
||||
* class even when the body accepts other objects with the same members.
|
||||
* Null overall when nothing qualifies — the declaration keeps its ABI. */
|
||||
export function implicitAnyParamSymbolsOf(lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,): (ts.Symbol | null)[] | null {
|
||||
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
|
||||
classParams = false,): (ts.Symbol | null)[] | null {
|
||||
if (!decl.body) return null;
|
||||
if (decl.asteriskToken) return null;
|
||||
if (decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
|
||||
@@ -2193,7 +2209,9 @@ function runtimeOptionalHofGenericBinding(
|
||||
const broadFunction = lowerer.checker.typeToString(t) === "Function";
|
||||
const broadArray = lowerer.checkerAnyArrayType(t);
|
||||
const broadPromise = broadPromiseParam(lowerer, t);
|
||||
if ((t.flags & ts.TypeFlags.Any) === 0 && !broadFunction && !broadArray && !broadPromise) return null;
|
||||
const declaredKind = classParams ? lowerer.mapTypeOf(t)?.kind : undefined;
|
||||
const classParam = classParams && (declaredKind === "object" || declaredKind === "array");
|
||||
if ((t.flags & ts.TypeFlags.Any) === 0 && !broadFunction && !broadArray && !broadPromise && !classParam) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!sym) return null;
|
||||
if (paramWrittenInBody(lowerer, decl.body!, sym, param.name.text)) return null;
|
||||
@@ -2210,6 +2228,11 @@ function runtimeOptionalHofGenericBinding(
|
||||
* selects return INFERENCE with DYN as the recursion pin. */
|
||||
function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | null {
|
||||
try {
|
||||
// Static JS helpers can accept a structurally compatible class despite
|
||||
// narrower JSDoc (for example a Vector2 target documented as Vector3).
|
||||
// Infer their result from the specialized body as well.
|
||||
if (info.member?.kind === "static" && info.decl.parameters.some((param, index) =>
|
||||
info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "object")) return null;
|
||||
const broadCallbackSymbols = new Set(
|
||||
info.decl.parameters.flatMap((param, index) => {
|
||||
const symbol = info.implicitParams?.[index];
|
||||
@@ -2247,6 +2270,8 @@ function runtimeOptionalHofGenericBinding(
|
||||
if (retTs.flags & ts.TypeFlags.Any) return null;
|
||||
if (retTs.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return DYN;
|
||||
if (lowerer.checker.isArrayType(retTs)) {
|
||||
if (info.decl.parameters.some((param, index) => info.implicitParams?.[index] &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array")) return null;
|
||||
const elem = lowerer.checker.getTypeArguments(retTs as ts.TypeReference)[0];
|
||||
// An implicit-any parameter poisons an inferred array result to
|
||||
// any[] even when this instance's body produces one concrete
|
||||
@@ -2412,7 +2437,17 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
|
||||
const shapes: ParamShape[] = info.decl.parameters.map((param, i) =>
|
||||
info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param),
|
||||
);
|
||||
return internImplicitInstance(lowerer, blame, info, shapes, new Map());
|
||||
const argTypes = new Map<ts.Symbol, ts.Type>();
|
||||
// Newly bindable nominal class parameters must not retain their JSDoc
|
||||
// receiver type in the checked fallback. Keep existing broad JS inference.
|
||||
for (let i = 0; i < info.decl.parameters.length; i++) {
|
||||
const symbol = info.implicitParams?.[i];
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind;
|
||||
if (symbol && (kind === "object" || kind === "array")) {
|
||||
argTypes.set(symbol, lowerer.checker.getUnknownType());
|
||||
}
|
||||
}
|
||||
return internImplicitInstance(lowerer, blame, info, shapes, argTypes);
|
||||
}
|
||||
|
||||
function internImplicitInstance(lowerer: Lowerer, blame: ts.Node,
|
||||
@@ -6664,6 +6699,12 @@ const inliningPredicates = new Set<ts.Symbol>();
|
||||
if (ts.isIdentifier(m.name) || ts.isStringLiteralLike(m.name)) own.add(m.name.text);
|
||||
else return null; // computed static names — the answer isn't static
|
||||
}
|
||||
if (!own.has(key)) {
|
||||
const loc = locOf(call);
|
||||
const data = classStaticDataFor(lowerer, info, key, loc);
|
||||
if (data) return { kind: "libCall", fn: "dyn.hasOwn", args: [data,
|
||||
{ kind: "strLit", value: key, type: STRING, loc }], type: BOOL, loc };
|
||||
}
|
||||
return { kind: "boolLit", value: own.has(key), type: BOOL, loc: locOf(call) };
|
||||
}
|
||||
|
||||
@@ -6886,6 +6927,12 @@ const inliningPredicates = new Set<ts.Symbol>();
|
||||
function reconcileOverloadReturn(lowerer: Lowerer, expr: ts.CallExpression | ts.TaggedTemplateExpression, call: IrExpr): IrExpr {
|
||||
const rsig = lowerer.checker.getResolvedSignature(expr);
|
||||
const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined;
|
||||
// Fluent JS overrides share the base's return ABI, but their result
|
||||
// still has the receiver's subclass layout at the call site.
|
||||
if (call.type.kind === "object" && rdecl && ts.isMethodDeclaration(rdecl) &&
|
||||
isJsSourceFile(rdecl.getSourceFile()) && returnsOnlyThis(rdecl)) {
|
||||
call = lowerer.maybeNarrow(call, expr);
|
||||
}
|
||||
const resolvedOverload =
|
||||
rsig && rdecl &&
|
||||
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
|
||||
@@ -9419,6 +9466,15 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
(member === "getOwnPropertyDescriptor" && call.arguments.length === 2)) {
|
||||
if (call.arguments.some((a) => ts.isSpreadElement(a))) return null;
|
||||
let target = tryLowerExpression(lowerer, call.arguments[0]!);
|
||||
if (target && member === "defineProperty") {
|
||||
const native = lowerClassDataDescriptor(lowerer, call, member, target);
|
||||
if (native) return native;
|
||||
}
|
||||
if (target && member === "getOwnPropertyDescriptor" && isDynTypedRefType(target.type)) {
|
||||
const descriptor = lowerClassDescriptorRead(lowerer, call, target);
|
||||
if (descriptor) return descriptor;
|
||||
target = lowerer.coerceToExpected(target, DYN);
|
||||
}
|
||||
// Error.cause owns a live data-property slot shared with its checked
|
||||
// view. Other native fields do not yet synchronize descriptor edits.
|
||||
const keyNode = call.arguments[1]!;
|
||||
@@ -9453,6 +9509,10 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
if (member === "defineProperties" && call.arguments.length === 2 &&
|
||||
!call.arguments.some((a) => ts.isSpreadElement(a))) {
|
||||
let target = tryLowerExpression(lowerer, call.arguments[0]!);
|
||||
if (target) {
|
||||
const native = lowerClassDataDescriptor(lowerer, call, member, target);
|
||||
if (native) return native;
|
||||
}
|
||||
// A FUNCTION-typed target boxes through the dyn boundary: the
|
||||
// property table lives on the CLOSURE (shared by every box of this
|
||||
// function value), so defining through a fresh box sticks — the
|
||||
@@ -9687,7 +9747,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
const loc = locOf(call);
|
||||
const resultT = lowerer.irTypeOf(call);
|
||||
if (resultT.kind !== "array") lowerer.badType(call, lowerer.typeOf(call)); // defensive
|
||||
const receiver = lowerer.lowerExpr(argNode);
|
||||
const receiver = objectEnumerationReceiver(lowerer, lowerer.lowerExpr(argNode), argIr, loc);
|
||||
if (member === "keys") {
|
||||
// The keys walk is shared with for-in (which iterates exactly the
|
||||
// keys Object.keys answers — one construction, one intern key).
|
||||
@@ -10026,7 +10086,9 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
export function lowerRecordFieldCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (lowerer.chainBlocked(call)) return null;
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type : undefined;
|
||||
const receiverType = stored?.kind === "record" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverType?.kind !== "record" && receiverType?.kind !== "union") return null;
|
||||
// A union of records can share a closure-valued field. Its normal
|
||||
// property read performs the tag dispatch before arguments evaluate.
|
||||
@@ -11004,7 +11066,15 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
export function lowerObjectMethodCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
// A specialized JS parameter can expose a native field whose checker
|
||||
// type still belongs to the unspecialized body (plane.normal.dot()).
|
||||
// Use the lowered receiver's representation and retain its evaluation.
|
||||
const probe = mappedReceiver?.kind !== "object" && (lowerer.implicitParamTypes !== null || ts.isNewExpression(access.expression) ||
|
||||
isJsSourceFile(call.getSourceFile()) && ts.isCallExpression(access.expression))
|
||||
? tryLowerExpression(lowerer, access.expression) : null;
|
||||
const specializedReceiver = probe?.type.kind === "object" ? probe : null;
|
||||
if (specializedReceiver) mappedReceiver = specializedReceiver.type;
|
||||
if (mappedReceiver?.kind === "union") {
|
||||
const dispatched =
|
||||
lowerUnionObjectMethodCall(lowerer, call, access, mappedReceiver) ??
|
||||
@@ -11025,7 +11095,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
: null;
|
||||
if (receiverIr === null) return null;
|
||||
const lowerReceiver = (): IrExpr => {
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = specializedReceiver ?? lowerer.lowerExpr(access.expression);
|
||||
const optional = lowerer.runtimeOptionalPropertyReceiver(
|
||||
access.expression,
|
||||
receiver,
|
||||
@@ -11061,7 +11131,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
// (lowerClassGenericMethodCall has the exactness rules).
|
||||
if (info && !found) {
|
||||
const gfound = findGenericMethodOn(lowerer, info, access.name.text);
|
||||
if (gfound) return lowerClassGenericMethodCall(lowerer, call, access, info, gfound);
|
||||
if (gfound) return lowerClassGenericMethodCall(lowerer, call, access, info, gfound, lowerReceiver());
|
||||
}
|
||||
// A FUNC-, nullable-FUNC-, or DYN-typed FIELD in call position:
|
||||
// `this.cb()` — the ctor-assigned callback field (countdown.js's
|
||||
@@ -11123,6 +11193,21 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
}
|
||||
if (!info || !found) return null;
|
||||
const method = access.name.text;
|
||||
if (isClassCallback(lowerer, info, method)) {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerReceiver();
|
||||
const local = lowerer.declareHiddenLocal("%callbackReceiver", receiver.type);
|
||||
const value: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc };
|
||||
const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call);
|
||||
const virtual = lowerer.overrideBelow(info, method);
|
||||
if (virtual) lowerer.noteVirtualEdge(info, method);
|
||||
else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
|
||||
const fallback: IrExpr = virtual
|
||||
? { kind: "virtualCall", className: info.def.name, method, args: [lowerer.upcastTo(value, info.def.name), ...args], type: found.sig.ret, loc }
|
||||
: { kind: "call", callee: `%${found.declarer.def.name}.${method}`, args: [lowerer.upcastTo(value, found.declarer.def.name), ...args], type: found.sig.ret, loc };
|
||||
const result = classCallbackCall(lowerer, value, method, call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), fallback, loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], result, type: result.type, loc };
|
||||
}
|
||||
if (found.declarer.builtinError) {
|
||||
return errorToStringCall(lowerer, lowerReceiver());
|
||||
}
|
||||
@@ -11205,7 +11290,10 @@ function lowerUnionObjectMethodCall(
|
||||
function paramAbisEqual(left: readonly ParamShape[], right: readonly ParamShape[]): boolean {
|
||||
return left.length === right.length && left.every((shape, i) => {
|
||||
const other = right[i];
|
||||
return other !== undefined && shape.mode === other.mode && typeEquals(shape.type, other.type);
|
||||
return other !== undefined && typeEquals(shape.type, other.type) &&
|
||||
(shape.mode === other.mode || shape.type.kind === "dyn" &&
|
||||
(shape.mode === "required" || shape.mode === "omittable") &&
|
||||
(other.mode === "required" || other.mode === "omittable"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { FnCtx, Lowerer } from "./lowerer.js";
|
||||
import { BOOL, DATE_T, DYN, F64, bytesOf, type IrClassDef, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js";
|
||||
import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js";
|
||||
import { isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
|
||||
import { isGenericCallableMemberType, jsOpenObjectType, typeKey, withUnitArm } from "../type-mapper.js";
|
||||
import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js";
|
||||
import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js";
|
||||
import { lowerArrayConstructor, lowerMapSeedNew, lowerSetSeedNew } from "./lower-containers.js";
|
||||
@@ -24,10 +24,31 @@ import { builtinFenceHintOf } from "./surfaces.js";
|
||||
import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js";
|
||||
import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, fenceEarlyAliasUse, fenceEarlyNsMemberRef, nsMemberIdentOf, nsUndefRead } from "./lower-namespaces.js";
|
||||
import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixins.js";
|
||||
import { rejectStaticThis } from "./static-this.js";
|
||||
import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./static-this.js";
|
||||
import { lowerUrlNew } from "./lower-url.js";
|
||||
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
|
||||
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
||||
const body = member.body;
|
||||
const last = body?.statements.at(-1);
|
||||
if (!body || !last || !ts.isReturnStatement(last)) return false;
|
||||
let valid = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node)) return;
|
||||
if (ts.isReturnStatement(node)) {
|
||||
let value = node.expression;
|
||||
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return valid;
|
||||
}
|
||||
|
||||
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
|
||||
@@ -41,6 +62,10 @@ export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression
|
||||
const member = nsMemberIdentOf(lowerer, expression);
|
||||
if (member) return storedClassValueType(lowerer, member);
|
||||
}
|
||||
if (expression.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const stored = lowerer.resolveThis();
|
||||
if (stored?.type.kind === "classval") return stored.type;
|
||||
}
|
||||
if (ts.isIdentifier(expression)) {
|
||||
const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression);
|
||||
if (stored?.type.kind === "classval") return stored.type;
|
||||
@@ -95,7 +120,7 @@ export interface ClassInfo {
|
||||
* no-dynamic-dispatch semantics by construction). A `gen` entry has a
|
||||
* generator body whose direct calls enter through its spawn wrapper. */
|
||||
methods: Map<string, ClassMethodSignature>;
|
||||
/** Own JS overrides whose return cannot use the inherited ABI. The slot
|
||||
/** Own JS overrides whose signature cannot use the inherited ABI. The slot
|
||||
* retains that ABI, but its implementation throws before executing. */
|
||||
methodEntryFences?: Map<string, ScrDiagnostic>;
|
||||
/** OWN GENERIC instance methods (own type parameters — `m<T>(x: T)`),
|
||||
@@ -183,14 +208,16 @@ export interface ClassInfo {
|
||||
* same dynamic story: both are named fences). Accessors and
|
||||
* initializer-less fields keep the fence. */
|
||||
staticFields: { name: string; type: IrType; initializer: ts.Expression; globalId: string; readonly: boolean }[];
|
||||
/** Shared expando data added after a JS class declaration. */
|
||||
staticDataHelper?: string;
|
||||
/** STATIC methods — ordinary module functions named `%C.static:m` (the
|
||||
* accessor-colon trick: no user identifier can spell it, and statics
|
||||
* never join vtables, so IrClassDef doesn't know them). `C.m(args)` is
|
||||
* a direct call; `const f = C.m` a zero-capture closure; calls through
|
||||
* class VALUES devirtualize when no strict descendant redeclares the
|
||||
* member. `this`/`super` inside fence at lowering (JS binds `this` to
|
||||
* the RECEIVER class — dynamic). Absent on builtin classes. */
|
||||
staticMethods?: Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }>;
|
||||
* member. Receiver-dependent methods specialize for exact receivers;
|
||||
* detached methods and `super` retain their fences. Absent on builtins. */
|
||||
staticMethods?: Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }>;
|
||||
/** `static { ... }` blocks, in declaration order. They are DECLARATION-TIME
|
||||
* CODE, not shape: JS runs each block once when the class statement
|
||||
* evaluates, whether or not anything ever references the class — so their
|
||||
@@ -551,16 +578,48 @@ export interface GenericClassInfo {
|
||||
return declared ? (lowerer.classes.get(irName) ?? null) : null;
|
||||
}
|
||||
|
||||
/** A dictionary lookup may be absent even when its checker type is a
|
||||
* required reference. Keep literal nullish fallbacks in inferred JS storage. */
|
||||
function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.Expression): IrType {
|
||||
let node = expression;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (node.kind === ts.SyntaxKind.NullKeyword) return withUnitArm(type, "nullT", lowerer.unions) ?? type;
|
||||
const inferred = lowerer.typeOf(node);
|
||||
for (const arm of inferred.isUnionType() ? ts.constituentTypes(inferred) : [inferred]) {
|
||||
if ((arm.flags & ts.TypeFlags.Undefined) !== 0) type = lowerer.withUndefinedArmOf(type) ?? type;
|
||||
if ((arm.flags & ts.TypeFlags.Null) !== 0) type = withUnitArm(type, "nullT", lowerer.unions) ?? type;
|
||||
}
|
||||
if (ts.isConditionalExpression(node)) {
|
||||
return jsFieldInitializerType(lowerer, jsFieldInitializerType(lowerer, type, node.whenTrue), node.whenFalse);
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) type = jsFieldInitializerType(lowerer, type, node.left);
|
||||
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) return jsFieldInitializerType(lowerer, type, node.right);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/** The undefined-armed union of a JS class property's inferred type — the
|
||||
* honest slot for a field first assigned outside the constructor's top
|
||||
* level (undefined until the write runs, Node-exact). Null when the
|
||||
* inference is unmappable, checked-dynamic (dyn stays out of class
|
||||
* fields — KEEP NARROW), or an arm-less kind that cannot join a union
|
||||
* inference is unmappable or an arm-less kind that cannot join a union
|
||||
* (genResultRecord's list, including scalar-backed Date values). */
|
||||
function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
|
||||
const t = lowerer.checker.getTypeOfSymbol(p);
|
||||
const mapped = lowerer.mapTypeOf(t);
|
||||
if (!mapped || mapped.kind === "void" || mapped.kind === "dyn") return null;
|
||||
const site = lowerer.checker.declarationsOf(p)[0];
|
||||
let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null);
|
||||
if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void") return null;
|
||||
for (const declaration of lowerer.checker.declarationsOf(p)) {
|
||||
const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent;
|
||||
if (assignment && ts.isBinaryExpression(assignment) && assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
mapped = jsFieldInitializerType(lowerer, mapped, assignment.right);
|
||||
}
|
||||
}
|
||||
// An implicit JS value already carries undefined in its native box.
|
||||
if (mapped.kind === "dyn") return mapped;
|
||||
const byKey = new Map<string, IrType>();
|
||||
const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
|
||||
for (const a of arms) {
|
||||
@@ -1293,7 +1352,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
const paramProps: NonNullable<ClassInfo["paramProps"]> = [];
|
||||
|
||||
const staticFields: ClassInfo["staticFields"] = [];
|
||||
const staticMethods = new Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }>();
|
||||
const staticMethods: NonNullable<ClassInfo["staticMethods"]> = new Map();
|
||||
const staticBlocks: ts.ClassStaticBlockDeclaration[] = [];
|
||||
// GENERIC methods (own type parameters), instance and static: only
|
||||
// the SYNTAX is checked here — parameter/return types mention the
|
||||
@@ -1419,6 +1478,15 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
}
|
||||
staticMethods.set(member.name.text, { params: shapes, ret: ft.ret, member });
|
||||
if (implicitMonoFile(decl.getSourceFile()) && !functionLocalClass(decl) &&
|
||||
inst === undefined && decl.typeParameters === undefined) {
|
||||
const implicit = implicitAnyParamSymbolsOf(lowerer, member, true);
|
||||
if (implicit) genericStatics.set(member.name.text, {
|
||||
decl: member, baseName: member.name.text,
|
||||
qualifiedName: `%${className}.static:${member.name.text}%implicit`,
|
||||
typeParams: [], instances: new Map(), implicitParams: implicit,
|
||||
});
|
||||
}
|
||||
}
|
||||
// GENERIC static methods monomorphize like top-level generic
|
||||
// functions (`%C.static:m%n`), async ones included — a generic
|
||||
@@ -1918,7 +1986,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
!lowerer.findMethodOn(base, member.name.text) &&
|
||||
!findGenericMethodOn(lowerer, base, member.name.text)
|
||||
) {
|
||||
const implicit = implicitAnyParamSymbolsOf(lowerer, member);
|
||||
const implicit = implicitAnyParamSymbolsOf(lowerer, member, true);
|
||||
if (implicit) {
|
||||
genericMethods.set(member.name.text, {
|
||||
decl: member,
|
||||
@@ -1932,6 +2000,14 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
}
|
||||
}
|
||||
const { shapes, funcType: ft } = lowerer.lambdaSignature(member);
|
||||
// JS subclasses can add parameters to a method. Reserve checked
|
||||
// argument slots in the base before constructing its vtable; shorter
|
||||
// bodies ignore them and omitted call arguments become undefined.
|
||||
const virtualArity = lowerer.virtualJsMethodArity.get(member) ?? shapes.length;
|
||||
if (virtualArity > shapes.length && !bodyReadsArguments(member) &&
|
||||
shapes.every((p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"))) {
|
||||
while (shapes.length < virtualArity) shapes.push({ type: DYN, mode: "omittable" });
|
||||
}
|
||||
if (fields.has(mName)) {
|
||||
lowerer.unsupported("SC1090", member.name, "methods shadowing inherited fields");
|
||||
}
|
||||
@@ -1960,6 +2036,25 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
// against the STATIC receiver's shape, so `m(x?: number)` and
|
||||
// `m(x: number | undefined)` interchange soundly in overrides.
|
||||
const overridden = lowerer.findMethodOn(base, mName);
|
||||
if (overridden && isJsSourceFile(member.getSourceFile())) {
|
||||
// Fluent overrides return the same native receiver. Keep the
|
||||
// inherited return ABI and upcast `this` when lowering returns.
|
||||
if (overridden.sig.ret.kind === "object" && base &&
|
||||
(base.def.name === overridden.sig.ret.className || lowerer.isSubclassOf(base.def.name, overridden.sig.ret.className)) &&
|
||||
returnsOnlyThis(member)) ft.ret = overridden.sig.ret;
|
||||
// An unannotated JS parameter can use the base's typed ABI.
|
||||
// Defaults stay local to each declaration, so only inherited
|
||||
// required/optional slots participate in this refinement.
|
||||
for (let i = 0; i < shapes.length; i++) {
|
||||
const parameter = member.parameters[i];
|
||||
const inherited = overridden.sig.params[i];
|
||||
if (parameter && inherited && !parameter.type && !parameter.initializer && !parameter.dotDotDotToken &&
|
||||
shapes[i]!.type.kind === "dyn" && inherited.type.kind !== "dyn" &&
|
||||
(inherited.mode === "required" || inherited.mode === "omittable")) {
|
||||
shapes[i] = { ...inherited };
|
||||
}
|
||||
}
|
||||
}
|
||||
// JS overrides may ignore trailing arguments. Retain the base's
|
||||
// checked-value ABI slots as unused parameters, and let shorter
|
||||
// calls fill them with undefined. Typed tails and rest/arguments
|
||||
@@ -1979,23 +2074,26 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
!overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) ||
|
||||
!typeEquals(overridden.sig.ret, ft.ret))
|
||||
) {
|
||||
const returnOnlyJsOverride =
|
||||
const fencedJsOverride =
|
||||
isJsSourceFile(member.getSourceFile()) &&
|
||||
overridden.declarer.decl !== null && isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
|
||||
overridden.sig.params.length === shapes.length &&
|
||||
overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) &&
|
||||
member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true &&
|
||||
overridden.sig.async !== true &&
|
||||
member.asteriskToken === undefined && overridden.sig.gen === undefined;
|
||||
if (returnOnlyJsOverride) {
|
||||
if (fencedJsOverride) {
|
||||
// A dormant JS method must not poison the whole class. Keep
|
||||
// the inherited call/dispatch ABI and refuse at method entry;
|
||||
// lowering this body with the base return would change JS.
|
||||
// lowering this body with the base signature would change JS.
|
||||
const returnOnly = overridden.sig.params.length === shapes.length &&
|
||||
overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type));
|
||||
methodEntryFences.set(mName, unsupportedDiag(
|
||||
"SC1090",
|
||||
locOf(member.name),
|
||||
`overriding method '${mName}' with a different return type (the native return type must match the base declaration exactly)`,
|
||||
returnOnly
|
||||
? `overriding method '${mName}' with a different return type (the native return type must match the base declaration exactly)`
|
||||
: `overriding method '${mName}' with a different signature (the native parameter and return types must match the base declaration exactly)`,
|
||||
));
|
||||
shapes.splice(0, shapes.length, ...overridden.sig.params);
|
||||
ft.ret = overridden.sig.ret;
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
@@ -2299,6 +2397,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
type = inferredEmptyCollectionFieldType(lowerer, decl, name, rhs) ?? type;
|
||||
}
|
||||
if (!type || type.kind === "void") lowerer.badType(assign, t ?? lowerer.typeOf(assign));
|
||||
type = jsFieldInitializerType(lowerer, type, rhs);
|
||||
// A JSDoc claim the BODY contradicts (`@type {Command}`
|
||||
// assigned `undefined` — the lazy-init idiom): the
|
||||
// representation follows the body — the field widens to the
|
||||
@@ -2419,6 +2518,10 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
if (isJsSourceFile(decl.getSourceFile())) {
|
||||
const armed = undefArmedFieldType(lowerer, p);
|
||||
if (armed !== null) {
|
||||
// Unknown JS properties use the instance bag: they do not
|
||||
// exist until assigned, and a subclass may declare the same
|
||||
// name as an ordinary layout field.
|
||||
if (armed.kind === "dyn") continue;
|
||||
fields.set(p.name, armed);
|
||||
fieldOrder.push({ name: p.name, type: armed, initializer: undefined });
|
||||
continue;
|
||||
@@ -3055,7 +3158,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
* with its OWN storage, exactly JS). */
|
||||
export function findStaticOn(lowerer: Lowerer, info: ClassInfo | null, name: string):
|
||||
| { declarer: ClassInfo; field: ClassInfo["staticFields"][number]; method?: undefined }
|
||||
| { declarer: ClassInfo; method: { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }; field?: undefined }
|
||||
| { declarer: ClassInfo; method: { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }; field?: undefined }
|
||||
| null {
|
||||
for (let c = info; c; c = c.base) {
|
||||
const field = c.staticFields.find((s) => s.name === name);
|
||||
@@ -3174,7 +3277,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
* Inherited writes create a new subclass property in JS, so they must not
|
||||
* update the declaring class's global. */
|
||||
export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAccessExpression): { id: string; type: IrType } | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.expression)) return null;
|
||||
if (access.questionDotToken || (!ts.isIdentifier(access.expression) && access.expression.kind !== ts.SyntaxKind.ThisKeyword)) return null;
|
||||
const info = lowerer.exactClassOfReceiver(access.expression);
|
||||
if (!info) return null;
|
||||
const found = lowerer.findStaticOn(info, access.name.text);
|
||||
@@ -3196,8 +3299,8 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* generic member rejection names the site. */
|
||||
export function lowerStaticFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.questionDotToken) return null;
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
if (!ts.isIdentifier(expr.expression) && expr.expression.kind !== ts.SyntaxKind.ThisKeyword) return null;
|
||||
const symbol = ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : null;
|
||||
const info =
|
||||
(symbol ? lowerer.classBySymbol.get(symbol) : undefined) ??
|
||||
// A require binding over `module.exports = class {…}` (the alias
|
||||
@@ -3205,6 +3308,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
// or `C.name` below would fall through to paths that answer for
|
||||
// stdlib globals instead of this class.
|
||||
propertyAssignedClassInfoOf(lowerer, symbol) ??
|
||||
exactClassOfReceiver(lowerer, expr.expression) ??
|
||||
undefined;
|
||||
if (!info) return null;
|
||||
// A decorated name that can REBIND (a replacing decorator): the
|
||||
@@ -3248,6 +3352,8 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
if (expr.name.text === "name" && info.def.jsName !== undefined) {
|
||||
return { kind: "strLit", value: info.def.jsName, type: STRING, loc };
|
||||
}
|
||||
const data = classStaticDataFor(lowerer, info, expr.name.text, loc);
|
||||
if (data) return { kind: "dynKeyGet", value: data, key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3443,6 +3549,12 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* hit the declaring class's storage exactly (the shadowing hazards of
|
||||
* general class values don't arise). Null for everything else. */
|
||||
export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null {
|
||||
// A class-valued lexical this is introduced only for an exact static
|
||||
// receiver specialization; ordinary instance this remains an object.
|
||||
if (expr.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const receiver = lowerer.resolveThis();
|
||||
return receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) ?? null : null;
|
||||
}
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
if (!symbol) return null;
|
||||
@@ -3584,6 +3696,11 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
export function lowerStaticMethodCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (access.questionDotToken) return null;
|
||||
if (access.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const receiver = lowerer.resolveThis();
|
||||
const info = receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) : undefined;
|
||||
if (info) return staticCallOn(lowerer, call, access, info, false);
|
||||
}
|
||||
// `module.exports.describe()` in a module whose whole export IS a
|
||||
// class expression: the receiver is exactly that class (the kept
|
||||
// export assignment pins it) — the direct-name rules apply.
|
||||
@@ -3683,7 +3800,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
`calling the static member '${access.name.text}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`,
|
||||
);
|
||||
}
|
||||
const instance = genericCallInstance(lowerer, call, gfound.info);
|
||||
const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, gfound.info, info, throughValue));
|
||||
const args = lowerer.completeArgs(call.arguments, instance.params, loc, call);
|
||||
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
||||
}
|
||||
@@ -3694,6 +3811,12 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
`calling the static member '${access.name.text}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`,
|
||||
);
|
||||
}
|
||||
const specialized = findGenericStaticOn(lowerer, info, access.name.text);
|
||||
if (specialized?.declarer === found.declarer && specialized.info.implicitParams) {
|
||||
const instance = implicitCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, specialized.info, info, throughValue));
|
||||
const args = lowerer.completeArgs(call.arguments, instance.params, loc, call);
|
||||
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
||||
}
|
||||
if (found.field !== undefined) {
|
||||
// A func-typed static field in call position: read the global,
|
||||
// call through the value (the ctor-assigned-callback pattern).
|
||||
@@ -3710,12 +3833,44 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
}
|
||||
return { kind: "callValue", callee, args, type: found.field.type.ret, loc };
|
||||
}
|
||||
if (found.method.member.body && hasStaticThis(found.method.member)) {
|
||||
const generic = found.method.receiverInfo ??= {
|
||||
decl: found.method.member, baseName: access.name.text,
|
||||
qualifiedName: `%${found.declarer.def.name}.static:${access.name.text}`,
|
||||
typeParams: [], instances: new Map(), member: { cls: found.declarer, kind: "static" },
|
||||
};
|
||||
const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, generic, info, throughValue));
|
||||
return { kind: "call", callee: instance.name,
|
||||
args: lowerer.completeArgs(call.arguments, instance.params, loc, call), type: instance.returnType, loc };
|
||||
}
|
||||
const fnName = `%${found.declarer.def.name}.static:${access.name.text}`;
|
||||
lowerer.noteEdge(fnName);
|
||||
const args = lowerer.completeArgs(call.arguments, found.method.params, loc, call);
|
||||
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
|
||||
}
|
||||
|
||||
function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpression, method: GenericFnInfo,
|
||||
receiver: ClassInfo, throughValue: boolean): GenericFnInfo {
|
||||
if (!hasStaticThis(method.decl)) return method;
|
||||
// A private name in the inherited body belongs to the declaring class,
|
||||
// even when the receiver declares a different private with that spelling.
|
||||
// Do not resolve it by receiver name until the static ABI tracks the brand.
|
||||
if (method.member && method.member.cls !== receiver && hasLexicalPrivateReference(method.decl)) {
|
||||
lowerer.unsupported("SC1090", access, "private member references in an inherited receiver-dependent static method");
|
||||
}
|
||||
if (throughValue && !classValueIsExactlyOwn(lowerer, access.expression, receiver)) {
|
||||
lowerer.unsupported("SC1090", access, "receiver-dependent static methods through a polymorphic class value");
|
||||
}
|
||||
const instances = method.receiverSpecializations ??= new Map();
|
||||
let specialized = instances.get(receiver.def.name);
|
||||
if (!specialized) {
|
||||
specialized = { ...method, qualifiedName: `${method.qualifiedName}%on.${receiver.def.name}`,
|
||||
instances: new Map(), staticReceiver: receiver, receiverSpecializations: new Map() };
|
||||
instances.set(receiver.def.name, specialized);
|
||||
}
|
||||
return specialized;
|
||||
}
|
||||
|
||||
/** Static member access through a class VALUE (`X.m` where X is
|
||||
* classval-typed): devirtualized — the member resolves against the
|
||||
* static class's chain, exact when no strict descendant redeclares it
|
||||
@@ -3739,6 +3894,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
}
|
||||
if (
|
||||
!ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.kind !== ts.SyntaxKind.ThisKeyword &&
|
||||
// `module.exports.label` in a class-replaced CJS module: the
|
||||
// receiver is the exact exported class, and the read is
|
||||
// side-effect-free — as bindable as an identifier.
|
||||
@@ -3883,15 +4039,14 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* collected subclass graph cannot answer this question yet. The checker
|
||||
* hierarchy identifies the original declarations through aliases as well. */
|
||||
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
if (!files.some(implicitMonoFile)) return;
|
||||
if (!files.some(isJsSourceFile)) return;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
const names = node.members.flatMap((member) =>
|
||||
const members = node.members.filter((member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
||||
!member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)
|
||||
? [member.name.text] : [],
|
||||
!member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
|
||||
);
|
||||
if (node.heritageClauses?.length && names.length > 0) {
|
||||
if (node.heritageClauses?.length && members.length > 0) {
|
||||
const symbol = lowerer.typeOf(node).getSymbol();
|
||||
const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null;
|
||||
const seen = new Set<ts.Type>();
|
||||
@@ -3900,11 +4055,19 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
|
||||
if (target === undefined || !target.isClassOrInterface() || seen.has(target)) return;
|
||||
seen.add(target);
|
||||
for (const base of lowerer.checker.getBaseTypes(target)) {
|
||||
for (const name of names) {
|
||||
const property = lowerer.checker.getPropertyOfType(base, name);
|
||||
for (const member of members) {
|
||||
if (!ts.isIdentifier(member.name)) continue;
|
||||
const property = lowerer.checker.getPropertyOfType(base, member.name.text);
|
||||
for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) {
|
||||
if (ts.isMethodDeclaration(declaration) && implicitMonoFile(declaration.getSourceFile())) {
|
||||
if (ts.isMethodDeclaration(declaration) && isJsSourceFile(declaration.getSourceFile())) {
|
||||
lowerer.virtualJsMethods.add(declaration);
|
||||
if (isJsSourceFile(member.getSourceFile()) && !member.typeParameters &&
|
||||
!member.asteriskToken && !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) &&
|
||||
!bodyReadsArguments(member) &&
|
||||
member.parameters.every((p) => !p.initializer && !p.dotDotDotToken)) {
|
||||
lowerer.virtualJsMethodArity.set(declaration,
|
||||
Math.max(lowerer.virtualJsMethodArity.get(declaration) ?? 0, member.parameters.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3914,9 +4077,8 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
|
||||
if (instance) markBases(instance);
|
||||
}
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
for (const file of files) visit(file);
|
||||
for (const file of files) ts.walkPreorder(file, visit);
|
||||
}
|
||||
|
||||
/** The receiver's EXACT runtime class, when the expression proves it: a
|
||||
@@ -4776,65 +4938,74 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
|
||||
forward?: IrExpr[],): IrStmt[] {
|
||||
const out: IrStmt[] = [];
|
||||
let superSeen = false;
|
||||
for (const stmt of info.ctor!.body!.statements) {
|
||||
const superCall =
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isCallExpression(stmt.expression) &&
|
||||
stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
? stmt.expression
|
||||
: null;
|
||||
if (!superCall) {
|
||||
out.push(...lowerer.lowerStmts([stmt]));
|
||||
continue;
|
||||
}
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "total");
|
||||
}
|
||||
try {
|
||||
if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls");
|
||||
superSeen = true;
|
||||
if (info.callableBase) {
|
||||
if (superCall.arguments.some(ts.isSpreadElement)) lowerer.unsupported("SC1090", superCall, "spreading explicit super arguments into an ordinary constructor");
|
||||
const loc = locOf(stmt);
|
||||
const pack: IrExpr = { kind: "dynArrLit", elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, [pack], loc));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
const statements = info.ctor!.body!.statements;
|
||||
const entry = { stmts: statements, index: 0, ctx: lowerer.ctx, frame: lowerer.scopes[lowerer.scopes.length - 1]!, out };
|
||||
lowerer.activeStmtLists.push(entry);
|
||||
try {
|
||||
for (let index = 0; index < statements.length; index++) {
|
||||
entry.index = index;
|
||||
const stmt = statements[index]!;
|
||||
const superCall =
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isCallExpression(stmt.expression) &&
|
||||
stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
? stmt.expression
|
||||
: null;
|
||||
if (!superCall) {
|
||||
out.push(...lowerer.lowerStmts([stmt]));
|
||||
continue;
|
||||
}
|
||||
const base = superBaseOf(info)!;
|
||||
if (base.builtinEmitter && superCall.arguments.length > 0) {
|
||||
// @types/node admits super({ captureRejections }) — no lowering.
|
||||
lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')");
|
||||
}
|
||||
if (base.builtinStream) {
|
||||
// super(options?) into a runtime stream base: the stream spoke
|
||||
// parses the options and binds overridden underscore methods.
|
||||
out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
continue;
|
||||
}
|
||||
const args = forward !== undefined
|
||||
? forward
|
||||
: base.builtinError
|
||||
? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt)
|
||||
: base.builtinEmitter
|
||||
? []
|
||||
: lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt);
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt)));
|
||||
// super() returns → field initializers → parameter-property
|
||||
// assignments (Node's order, probed) → the rest of the body.
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "failed");
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "total");
|
||||
}
|
||||
try {
|
||||
if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls");
|
||||
superSeen = true;
|
||||
if (info.callableBase) {
|
||||
if (superCall.arguments.some(ts.isSpreadElement)) lowerer.unsupported("SC1090", superCall, "spreading explicit super arguments into an ordinary constructor");
|
||||
const loc = locOf(stmt);
|
||||
const pack: IrExpr = { kind: "dynArrLit", elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, [pack], loc));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
continue;
|
||||
}
|
||||
const base = superBaseOf(info)!;
|
||||
if (base.builtinEmitter && superCall.arguments.length > 0) {
|
||||
// @types/node admits super({ captureRejections }) — no lowering.
|
||||
lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')");
|
||||
}
|
||||
if (base.builtinStream) {
|
||||
// super(options?) into a runtime stream base: the stream spoke
|
||||
// parses the options and binds overridden underscore methods.
|
||||
out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
continue;
|
||||
}
|
||||
const args = forward !== undefined
|
||||
? forward
|
||||
: base.builtinError
|
||||
? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt)
|
||||
: base.builtinEmitter
|
||||
? []
|
||||
: lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt);
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt)));
|
||||
// super() returns → field initializers → parameter-property
|
||||
// assignments (Node's order, probed) → the rest of the body.
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lowerer.activeStmtLists.pop();
|
||||
}
|
||||
if (!superSeen) {
|
||||
// tsc guarantees the call exists somewhere; if it wasn't a top-level
|
||||
@@ -5205,6 +5376,24 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
||||
if (objectFactory) return objectFactory;
|
||||
let selected: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(selected)) selected = selected.expression;
|
||||
if (ts.isConditionalExpression(selected)) {
|
||||
const yes = exactClassOfReceiver(lowerer, selected.whenTrue);
|
||||
const no = exactClassOfReceiver(lowerer, selected.whenFalse);
|
||||
if (yes && no && !yes.classDecorators && !no.classDecorators) {
|
||||
const yesType: IrType = { kind: "object", className: yes.def.name };
|
||||
const noType: IrType = { kind: "object", className: no.def.name };
|
||||
const type: IrType = yes === no ? yesType : { kind: "union", unionId: lowerer.unions.intern([yesType, noType].sort((a, b) => typeKey(a) < typeKey(b) ? -1 : 1)) };
|
||||
return { kind: "ternary", cond: lowerer.lowerCondition(selected.condition),
|
||||
then: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, yes, loc), type),
|
||||
else_: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, no, loc), type), type, loc };
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "Segmenter" &&
|
||||
lowerer.isStdlibGlobal(expr.expression.expression, "Intl")) {
|
||||
const args = expr.arguments ?? [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { objectEnumerationReceiver } from "./object-enumeration-receiver.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Container-surface call lowering: array methods (including the HOF family
|
||||
* map/filter/forEach with their synthesized helper functions), Map/Set
|
||||
@@ -2751,7 +2752,17 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc:
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const argNode = call.arguments[0]!;
|
||||
const fnArg = lowerer.lowerExpr(argNode);
|
||||
let fnArg = lowerer.lowerExpr(argNode);
|
||||
// Reusable JS comparators often accept checked values. Adapt the array's
|
||||
// element ABI just as other array callbacks do before sorting.
|
||||
if (fnArg.type.kind === "func" && fnArg.type.params.length <= 2 &&
|
||||
fnArg.type.params.every((param) => lowerer.coercibleValue(elem, param))) {
|
||||
// Arithmetic on inferred JS values retains a checked result because
|
||||
// it can produce BigInt. The sort callback still requires a number.
|
||||
const result = fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile()) ? F64 : fnArg.type.ret;
|
||||
const expected = funcOf(fnArg.type.params.map(() => elem), result);
|
||||
if (!typeEquals(fnArg.type, expected)) fnArg = lowerer.coerceToExpected(fnArg, expected);
|
||||
}
|
||||
// The comparator receives exactly (a, b); declaring a prefix is
|
||||
// ordinary TS. Its result must be number (the spec coerces arbitrary
|
||||
// results — no lowering for that).
|
||||
@@ -6001,7 +6012,8 @@ function mapFromSeedValue(lowerer: Lowerer, seed: IrExpr, mapT: IrType & { kind:
|
||||
shape: IrRecordShape,): IrExpr {
|
||||
const resultT = lowerer.irTypeOf(call);
|
||||
if (resultT.kind !== "array") lowerer.badType(call, lowerer.typeOf(call)); // defensive
|
||||
return objectIterOverIndexShape(lowerer, call, member, argIr, shape, lowerer.lowerExpr(call.arguments[0]!), resultT, locOf(call));
|
||||
return objectIterOverIndexShape(lowerer, call, member, argIr, shape,
|
||||
objectEnumerationReceiver(lowerer, lowerer.lowerExpr(call.arguments[0]!), argIr, locOf(call)), resultT, locOf(call));
|
||||
}
|
||||
|
||||
/** The construction core, receiver/result pre-resolved — `node` anchors
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstO
|
||||
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
|
||||
import { lowerCollectionSpread, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js";
|
||||
import { arrayValueRead, arrayValueStore } from "./array-values.js";
|
||||
import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values.js";
|
||||
import { lowerOptionalStringIndex } from "./string-index.js";
|
||||
import { tryLowerIndexedComparison } from "./indexed-comparison.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
@@ -32,7 +32,7 @@ import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.
|
||||
import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget, storedClassValueType } from "./lower-classes.js";
|
||||
import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
|
||||
import { mixinFnOfCallee } from "./lower-mixins.js";
|
||||
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion, withUnitArm } from "../type-mapper.js";
|
||||
import { lowerYield } from "./lower-generators.js";
|
||||
import { errorToStringCall } from "./error-methods.js";
|
||||
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
|
||||
@@ -50,6 +50,7 @@ import { coerceStringSearchValue, defaultAfterUndefined, lowerStaticallyUndefine
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { classSymbolKeyOf } from "./symbol-fields.js";
|
||||
import { lowerClassMethodValue } from "./class-method-values.js";
|
||||
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
||||
import { lowerGlobalValue } from "./lower-global-value.js";
|
||||
import { lowerClassPrototypeData } from "./class-prototypes.js";
|
||||
import { lowerArrayIsArrayValue } from "./lower-builtin-values.js";
|
||||
@@ -674,8 +675,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// every function and constructor global is "function" (the harness's
|
||||
// `typeof queueMicrotask === 'function'` probes). Shadowing locals
|
||||
// have non-stdlib symbols and keep the ordinary path.
|
||||
if (ts.isIdentifier(expr.expression)) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr.expression);
|
||||
let tested = expr.expression;
|
||||
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
|
||||
if (ts.isIdentifier(tested)) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(tested);
|
||||
if (!sym && !lowerer.dynamic) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
const t = lowerer.typeOf(expr.expression);
|
||||
if (
|
||||
@@ -1523,6 +1529,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
|
||||
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
// Expando statics may still be absent even when JS inference sees
|
||||
// the eventual assignment's scalar type. Preserve the checked value.
|
||||
const staticField = lowerStaticFieldRead(lowerer, expr);
|
||||
if (staticField) return staticField;
|
||||
const prototypeData = lowerClassPrototypeData(lowerer, expr);
|
||||
if (prototypeData) return prototypeData;
|
||||
// `super.x`: the base chain's GETTER, called directly (super
|
||||
@@ -1951,6 +1961,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
expr,
|
||||
);
|
||||
}
|
||||
if (expr.name.text === "length" && (recv.type.kind === "array" || recv.type.kind === "string")) {
|
||||
return recv.type.kind === "array"
|
||||
? { kind: "arrIntrinsic", method: "length", receiver: recv, args: [], type: F64, loc }
|
||||
: { kind: "strIntrinsic", method: "length", receiver: recv, args: [], type: F64, loc };
|
||||
}
|
||||
}
|
||||
// `f.name` / `f.length` / own properties on a FUNCTION-typed JS
|
||||
// value (the mustCall wrapper's function-instance members): read
|
||||
@@ -2177,8 +2192,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
`reading the abstract property '${expr.name.text}' through a '${lowerer.checker.typeToString(lowerer.typeOf(expr.expression))}'-typed receiver (abstract property declarations are erased at runtime, so no shared slot exists — type the receiver as the concrete class, or declare an abstract getter instead)`,
|
||||
);
|
||||
}
|
||||
if (isDynTypedRefType(recvLowered.type)) {
|
||||
const info = lowerer.classes.get(recvLowered.type.className);
|
||||
// Array reads may add an undefined arm even when the checker still
|
||||
// reports a class. Its property bag must remain reachable, while
|
||||
// an absent element takes the normal checked-value TypeError.
|
||||
const nativeReceiver = lowerer.stripUndefinedArm(recvLowered.type);
|
||||
if (isDynTypedRefType(nativeReceiver)) {
|
||||
const info = lowerer.classes.get(nativeReceiver.className);
|
||||
if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) {
|
||||
return lowerer.maybeNarrow({
|
||||
kind: "dynKeyGet", value: lowerer.coerceToExpected(recvLowered, DYN),
|
||||
@@ -4733,7 +4752,7 @@ export function lowerOptionalNumber(
|
||||
// (`pkg.workspaces.packages["0"]` — the lowering world types the
|
||||
// unknown-rooted chain `any`); a non-dyn lowering falls through to
|
||||
// the fences below (re-lowering is pure IR construction).
|
||||
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
||||
if (receiverIr?.kind === "dyn" || receiverIr?.kind === "union" || receiverIr === null) {
|
||||
const obj = lowerer.lowerExpr(expr.expression);
|
||||
// A generic mapped type can stay unresolved at this body use even
|
||||
// though its instantiated parameter has a concrete record ABI.
|
||||
@@ -4744,7 +4763,7 @@ export function lowerOptionalNumber(
|
||||
return lowerer.lowerRecordKeyRead(expr, obj.type.shapeId, shape);
|
||||
}
|
||||
}
|
||||
if (obj.type.kind === "dyn") {
|
||||
if (obj.type.kind === "dyn" || isDynTypedRefType(obj.type)) {
|
||||
const rawKey = lowerer.lowerExpr(expr.argumentExpression);
|
||||
// Number, bool, and DYN keys stringify (ToPropertyKey) — the
|
||||
// dyn-keyed read `catchWarning[warning.name]` where the property
|
||||
@@ -4755,7 +4774,7 @@ export function lowerOptionalNumber(
|
||||
if (key.type.kind === "string" || key.type.kind === "dyn") {
|
||||
const opt = hasOptionalChainGuard(expr.expression);
|
||||
return lowerer.maybeNarrow(
|
||||
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: obj, type: DYN, loc: locOf(expr) },
|
||||
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: lowerer.coerceToExpected(obj, DYN), type: DYN, loc: locOf(expr) },
|
||||
expr,
|
||||
);
|
||||
}
|
||||
@@ -5217,6 +5236,10 @@ export function lowerOptionalNumber(
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
const target = lowerer.fieldTarget(expr);
|
||||
if (target?.container === "class" && target.fieldType.kind === "union" &&
|
||||
lowerer.unions.get(target.fieldType.unionId)?.arms.some(isUnitType)) {
|
||||
return lowerer.fieldGetExpr(target, locOf(expr), expr);
|
||||
}
|
||||
if (target?.container !== "recordOvf") return null;
|
||||
// A JS file-scope object-literal global is record-shaped to the
|
||||
// checker but stored in the checked-dynamic tree to preserve object
|
||||
@@ -5796,9 +5819,9 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
// type (static `any` — mapTypeOf answers null without --dynamic)
|
||||
// probes the receiver's own lowered world: a dyn value takes the same
|
||||
// write, anything else falls through to the fences.
|
||||
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
||||
if (receiverIr?.kind === "dyn" || receiverIr?.kind === "object" || receiverIr === null) {
|
||||
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
|
||||
if (obj !== null && obj.type.kind === "dyn") {
|
||||
if (obj !== null && (obj.type.kind === "dyn" || isDynTypedRefType(obj.type))) {
|
||||
const loc = locOf(expr);
|
||||
const litKey = recordKeyLiteralText(target.argumentExpression);
|
||||
let key: IrExpr =
|
||||
@@ -5817,7 +5840,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
`storing '${lowerer.fmt(value.type)}' values in a checked-dynamic object (the value cannot convert into the checked-dynamic tree)`,
|
||||
);
|
||||
}
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [obj, key, value], type: VOID, loc }, loc };
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [lowerer.coerceToExpected(obj, DYN), key, value], type: VOID, loc }, loc };
|
||||
}
|
||||
}
|
||||
if (receiverIr?.kind !== "array") {
|
||||
@@ -5849,7 +5872,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
if (arr.type.kind !== "array") {
|
||||
lowerer.unsupported("SC1090", target.expression, "assignment through a possibly missing nested array receiver");
|
||||
}
|
||||
const index = lowerer.lowerExpr(target.argumentExpression);
|
||||
const index = lowerOptionalNumber(lowerer, lowerer.lowerExpr(target.argumentExpression), locOf(target.argumentExpression), target.argumentExpression);
|
||||
if (index.type.kind !== "f64") {
|
||||
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
||||
}
|
||||
@@ -5860,7 +5883,13 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
while (ts.isParenthesizedExpression(literal) || ts.isSatisfiesExpression(literal)) literal = literal.expression;
|
||||
const value = ts.isObjectLiteralExpression(literal) || ts.isArrayLiteralExpression(literal)
|
||||
? lowerer.lowerExprExpecting(literal, arr.type.elem) : lowerer.lowerExpr(expr.right);
|
||||
return arrayValueStore(lowerer, arr, index, value, arr.type.elem, locOf(expr));
|
||||
// Validate the payload before emitting a native store. A dormant JS
|
||||
// method can contradict its JSDoc element type; retain its call-time
|
||||
// refusal instead of leaving invalid IR in a newly reachable method.
|
||||
const expected = value.type.kind === "undefinedT" ||
|
||||
value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0
|
||||
? arrayValueType(lowerer, arr.type.elem) : arr.type.elem;
|
||||
return arrayValueStore(lowerer, arr, index, lowerer.coerceInto(expr.right, value, expected), arr.type.elem, locOf(expr));
|
||||
}
|
||||
|
||||
/** Environment property names use ToPrimitive with the string hint, so
|
||||
@@ -6282,7 +6311,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
if (raw.type.kind === "bigint") {
|
||||
return { kind: "libCall", fn: "bigint.neg", args: [raw], type: BIGINT_T, loc };
|
||||
}
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
// Match the checked numeric boundary used by JS binary arithmetic.
|
||||
const operand = raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
|
||||
? { kind: "dynCheck" as const, value: raw, type: F64, loc }
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") lowerer.unsupported("SC1043", expr);
|
||||
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
|
||||
return { kind: "unary", op: "-", operand, type: F64, loc };
|
||||
@@ -6859,7 +6891,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
const reference = scalarSide.type;
|
||||
if ((isDynTypedRefType(reference) || reference.kind === "record" || reference.kind === "array" ||
|
||||
reference.kind === "bytes" || reference.kind === "func" || reference.kind === "regex" || reference.kind === "url" ||
|
||||
reference.kind === "bigint" || reference.kind === "symbol" || reference.kind === "set" || DYN_HANDLE_KINDS.has(reference.kind)) &&
|
||||
reference.kind === "bigint" || reference.kind === "symbol" || reference.kind === "set" || reference.kind === "union" || DYN_HANDLE_KINDS.has(reference.kind)) &&
|
||||
lowerer.dynConvertible(reference)) {
|
||||
const boxed: IrExpr = {
|
||||
kind: "dynFrom", value: scalarSide, type: DYN, loc: scalarSide.loc,
|
||||
@@ -7175,6 +7207,15 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
}
|
||||
const optionalStringPlus = lowerOptionalStringPlus();
|
||||
if (optionalStringPlus) return optionalStringPlus;
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
const primitive = (type: IrType): boolean =>
|
||||
type.kind === "f64" || type.kind === "string" || type.kind === "bool" || isUnitType(type) ||
|
||||
(type.kind === "union" && (lowerer.unions.get(type.unionId)?.arms.every(primitive) ?? false));
|
||||
if ((left.type.kind === "union" || right.type.kind === "union") && primitive(left.type) && primitive(right.type)) {
|
||||
lowerer.runtimeOptionalArithmeticTypes.set(expr, DYN);
|
||||
return { kind: "libCall", fn: "dyn.add", args: [lowerer.coerceToExpected(left, DYN), lowerer.coerceToExpected(right, DYN)], type: DYN, loc };
|
||||
}
|
||||
}
|
||||
lowerer.unsupported("SC1043", expr);
|
||||
break;
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
@@ -7477,15 +7518,17 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// Test the deciding operand in its own representation. The result
|
||||
// contains only the values that can survive short-circuit evaluation.
|
||||
let target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// Unchecked array reads carry undefined even when the checker sees
|
||||
// a required value. The RHS can return it for either operator; the
|
||||
// LHS can return it only for &&. Preserve it through later chain pairs.
|
||||
if (target !== null && !isUnitType(target) && (
|
||||
(right.type.kind === "union" && lowerer.armTag(right.type.unionId, UNDEFINED_T) >= 0) ||
|
||||
(op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" &&
|
||||
lowerer.armTag(left.type.unionId, UNDEFINED_T) >= 0)
|
||||
)) {
|
||||
target = lowerer.runtimeOptionalType(target);
|
||||
// Index reads can be absent without noUncheckedIndexedAccess. Their
|
||||
// explicit nullish fallback still runs, even if the checker discarded it.
|
||||
if (target !== null) {
|
||||
const returned = [right.type, ...(op === ts.SyntaxKind.AmpersandAmpersandToken ? [left.type] : [])];
|
||||
for (const type of returned) {
|
||||
const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms ?? [] : [type];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind === "undefinedT" && !isUnitType(target)) target = lowerer.runtimeOptionalType(target);
|
||||
if (arm.kind === "nullT") target = withUnitArm(target, arm.kind, lowerer.unions) ?? target;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A broad-JSDoc npm-static body can leave the CHECKER result `any`
|
||||
// even after declaration-backed specialization has recovered both
|
||||
@@ -8518,6 +8561,9 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// (name strings are user-writable), so `u instanceof TypeError` keeps
|
||||
// the fence. Reads past the narrow bridge through maybeNarrow's
|
||||
// validated %Error extraction. SEMANTICS.md 67.
|
||||
if (left.type.kind === "dyn" && !target.def.runtime && !target.builtinError && !target.builtinEmitter && !target.builtinStream && !target.localClass) {
|
||||
return classInstanceOf(lowerer, left, target, loc);
|
||||
}
|
||||
if (left.type.kind === "dyn" && target.def.name === "%Error") {
|
||||
return { kind: "dynTest", test: "error", value: left, type: BOOL, loc };
|
||||
}
|
||||
@@ -10013,10 +10059,15 @@ function representedClassFieldTarget(
|
||||
if (access.name.text === "stackTraceLimit" && lowerer.isStdlibGlobal(access.expression, "Error")) {
|
||||
return { container: "errorStackLimit", obj: { kind: "numLit", value: 0, type: F64, loc: locOf(access) }, field: "stackTraceLimit", fieldType: F64 };
|
||||
}
|
||||
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type : undefined;
|
||||
const receiverIr = stored?.kind === "record" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverIr?.kind === "object") {
|
||||
return classFieldTarget(lowerer, access.expression, receiverIr, access.name.text);
|
||||
}
|
||||
if (receiverIr?.kind === "union" && hasClassPayload(lowerer, receiverIr)) {
|
||||
return representedClassFieldTarget(lowerer, access.expression, access.name.text, lowerer.lowerExpr(access.expression));
|
||||
}
|
||||
if (receiverIr?.kind === "record") {
|
||||
const shape = lowerer.shapes.get(receiverIr.shapeId);
|
||||
const fieldType = shape?.fields.find((f) => f.name === access.name.text)?.type;
|
||||
|
||||
@@ -3395,7 +3395,15 @@ export function lowerStaticReadableStreamReaderCall(
|
||||
ts.isSpreadElement(call.arguments[0]!)
|
||||
) {
|
||||
const spread = call.arguments[0]! as ts.SpreadElement;
|
||||
const src = lowerer.lowerExpr(spread.expression);
|
||||
let src = lowerer.lowerExpr(spread.expression);
|
||||
// JavaScript array call results retain checked storage so aliases stay
|
||||
// shared. This read-only fold can extract the inferred numeric array.
|
||||
if (src.type.kind === "dyn") {
|
||||
const inferred = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
|
||||
if (inferred?.kind === "array" && inferred.elem.kind === "f64") {
|
||||
src = lowerer.coerceInto(spread.expression, src, inferred);
|
||||
}
|
||||
}
|
||||
if (src.type.kind !== "array" || src.type.elem.kind !== "f64") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefi
|
||||
import { isNativeFfiRequire, builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, stripTypeCasts } from "./lower-builtins.js";
|
||||
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, funcTypeFromParamShapes, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
|
||||
import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
|
||||
import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js";
|
||||
@@ -1640,7 +1641,10 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
// and closures created in the init body capture it normally.
|
||||
// References from separately-declared functions cascade to
|
||||
// their own per-site runtime fences.
|
||||
if (isJsSourceFile(sf) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode))) continue;
|
||||
const factoryType = !isLet && isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) &&
|
||||
ts.isIdentifier(decl.name) && nameNode === decl.name && decl.initializer && ts.isNewExpression(decl.initializer)
|
||||
? objectFactorySignature(lowerer, decl.initializer)?.returnType : undefined;
|
||||
if (!factoryType && isJsSourceFile(sf) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode))) continue;
|
||||
// `var p1 = import("./m")` at file scope: the global holds the
|
||||
// island promise/handle — the import expression's only
|
||||
// production — whatever the checker's namespace type mapped to
|
||||
@@ -1655,7 +1659,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
// exactly the local rule (uncheckedOverloadHandleCall).
|
||||
(uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null)
|
||||
: null;
|
||||
let type = handleT ?? lowerer.irTypeOf(nameNode);
|
||||
let type = handleT ?? factoryType ?? lowerer.irTypeOf(nameNode);
|
||||
// Calls through checked function values retain their object
|
||||
// identity, including symbol keys absent from inferred records.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && type.kind === "record" &&
|
||||
@@ -1674,6 +1678,10 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
// checked factory. Keep that callable boxed instead of asking
|
||||
// a fixed-signature adapter to reconstruct its hidden rest ABI.
|
||||
if (!decl.type && !hasJsTypeAnnotation(decl) && type.kind === "func" && type.rest === true && type.restAbi === undefined) type = DYN;
|
||||
// JavaScript call results may retain a checked array supplied by
|
||||
// the caller. A typed global here would copy it on extraction.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && type.kind === "array" &&
|
||||
decl.initializer && ts.isCallExpression(decl.initializer)) type = DYN;
|
||||
// Unannotated JavaScript aliases retain an existing native
|
||||
// checked object instead of copying it into an inferred record.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) &&
|
||||
|
||||
@@ -23,6 +23,9 @@ import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingG
|
||||
import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
|
||||
import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js";
|
||||
import { isCompiledPrototypeMember } from "./class-prototypes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js";
|
||||
@@ -37,7 +40,7 @@ import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
|
||||
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { isParseArgsDynTypeName, isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { isParseArgsDynTypeName, isUnitOnlyTsType, jsOpenObjectType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { canonicalBuiltinModule } from "../builtin-modules.js";
|
||||
import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
@@ -3334,9 +3337,9 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
|
||||
);
|
||||
}
|
||||
|
||||
/** For-loop initializers evaluate left to right in the loop's head scope.
|
||||
* The backend copies each captured lexical binding before the condition
|
||||
* and before every update. Var declarations assign function-scoped slots. */
|
||||
/** For-loop initializers evaluate declarators from left to right. The LLVM
|
||||
* loop emitter keeps their scope alive and freshens every captured let
|
||||
* binding before the first condition and each subsequent update. */
|
||||
export function lowerVarDeclList(lowerer: Lowerer, list: ts.VariableDeclarationList): IrStmt | null {
|
||||
if ((list.flags & ts.NodeFlags.Using) !== 0) {
|
||||
lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)");
|
||||
@@ -3840,9 +3843,9 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
type = DYN;
|
||||
}
|
||||
const preservesObjectIdentity = init.type.kind === "dyn" &&
|
||||
(inferredObjectType?.kind === "record" ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType?.kind === "date") ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType?.kind === "func") ||
|
||||
(inferredObjectType?.kind === "record" || inferredObjectType?.kind === "array" ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(inferredObjectType.kind === "date" || inferredObjectType.kind === "func" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(isUnitType(inferredObjectType) || inferredObjectType.kind === "union" && lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType)))) &&
|
||||
!decl.type && !hasJsTypeAnnotation(decl);
|
||||
@@ -3975,6 +3978,9 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
}
|
||||
if (!type) lowerer.badType(decl.name, lowerer.typeOf(decl.name));
|
||||
let settledType: IrType = type;
|
||||
if (!isLet && isJsSourceFile(decl.getSourceFile()) && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isNewExpression(decl.initializer)) {
|
||||
settledType = objectFactorySignature(lowerer, decl.initializer)?.returnType ?? settledType;
|
||||
}
|
||||
const arithmeticType = decl.initializer ? lowerer.runtimeOptionalArithmeticTypes.get(decl.initializer) : undefined;
|
||||
const isStringArithmeticUnion = (t: IrType): boolean => {
|
||||
if (t.kind !== "union") return false;
|
||||
@@ -5189,13 +5195,20 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
// SUBCLASS name (`D.x = v` creates an OWN property on D in JS —
|
||||
// different storage) and through class VALUES (the same dynamic
|
||||
// story) are named fences, never a silently-wrong global write.
|
||||
if (!expr.left.questionDotToken && ts.isIdentifier(expr.left.expression)) {
|
||||
if (!expr.left.questionDotToken && (ts.isIdentifier(expr.left.expression) || expr.left.expression.kind === ts.SyntaxKind.ThisKeyword)) {
|
||||
// The receiver must BE the class exactly (its name, or a
|
||||
// const binding holding a class expression) — a general class
|
||||
// VALUE could hold a subclass, where JS creates an own
|
||||
// property instead of writing this storage.
|
||||
const classInfo = lowerer.exactClassOfReceiver(expr.left.expression);
|
||||
if (classInfo) {
|
||||
const data = classStaticDataFor(lowerer, classInfo, expr.left.name.text, locOf(expr.left));
|
||||
if (data) {
|
||||
const loc = locOf(expr);
|
||||
const key: IrExpr = { kind: "strLit", value: expr.left.name.text, type: STRING, loc };
|
||||
const value = lowerer.lowerExprExpecting(expr.right, DYN);
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [data, key, value], type: VOID, loc }, loc };
|
||||
}
|
||||
const found = lowerer.findStaticOn(classInfo, expr.left.name.text);
|
||||
if (found?.field !== undefined) {
|
||||
if (found.declarer !== classInfo) {
|
||||
@@ -5320,6 +5333,8 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
const value = lowerer.lowerExprExpecting(expr.right, target.fieldType);
|
||||
return lowerer.fieldSetStmt(target, value, locOf(expr), expr.left);
|
||||
}
|
||||
const callback = lowerClassCallbackAssign(lowerer, expr);
|
||||
if (callback) return callback;
|
||||
// A write to an ABSTRACT property through an abstract-typed
|
||||
// receiver: the read fence's write twin (the declaration is
|
||||
// erased at runtime — no shared slot exists to write).
|
||||
@@ -7185,7 +7200,9 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
return forValues(tmp.id, [write, ...body]);
|
||||
}
|
||||
const local = lowerer.declareLocal(decl.name, decl.name.text, elemValueT, isLet);
|
||||
if (!typeEquals(elemValueT, sourceT.elem)) {
|
||||
const declaredElement = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
|
||||
if (!typeEquals(elemValueT, sourceT.elem) ||
|
||||
(declaredElement !== null && lowerer.runtimeOptionalWidening(elemValueT, declaredElement) !== null)) {
|
||||
const root = lowerer.runtimeOptionalRootOf(local);
|
||||
lowerer.runtimeOptionalLocals.add(root);
|
||||
lowerer.runtimeOptionalStorageLocals.add(root);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
|
||||
import { buildUnionNarrow } from "./union-narrow.js";
|
||||
import { planUnionRetag, buildUnionRetag, planRecordUnionWrap, buildRecordUnionWrap } from "./union-retag.js";
|
||||
import type { WidthLift } from "./width-lift.js";
|
||||
@@ -1674,6 +1675,9 @@ export class Lowerer {
|
||||
/** Inferred JS methods participating in an override chain keep a vtable
|
||||
* ABI instead of call-site specialization. Filled before class collection. */
|
||||
readonly virtualJsMethods = new Set<ts.MethodDeclaration>();
|
||||
/** Largest fixed argument list below a JS method, discovered before any
|
||||
* base vtable signature is collected. Unused checked slots carry undefined. */
|
||||
readonly virtualJsMethodArity = new Map<ts.MethodDeclaration, number>();
|
||||
/** The class whose members are lowering — `super` binds lexically to it
|
||||
* (arrows inside methods lower within this window, so they see it too). */
|
||||
currentClass: ClassInfo | null = null;
|
||||
@@ -3450,8 +3454,12 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
}
|
||||
node.arguments.forEach((arg, i) => {
|
||||
const callbackSlot = sig.params[i]?.type;
|
||||
const restAt = sig.params.findIndex((shape) =>
|
||||
shape.mode === "rest" || shape.mode === "dynRest" || shape.mode === "islandRest" || shape.mode === "arguments");
|
||||
node.arguments.forEach((arg, argumentIndex) => {
|
||||
const i = restAt >= 0 && argumentIndex >= restAt ? restAt : argumentIndex;
|
||||
const shape = sig.params[i];
|
||||
const callbackSlot = shape?.type;
|
||||
if (callbackSlot?.kind === "func" && !ts.isSpreadElement(arg)) {
|
||||
const optionalCallbackParams = callbackSlot.params.flatMap((type, index) =>
|
||||
type.kind === "union" && this.armTag(type.unionId, UNDEFINED_T) >= 0 ? [index] : []);
|
||||
@@ -3459,27 +3467,28 @@ export class Lowerer {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (ts.isSpreadElement(arg) || !mayBeOptional(arg)) return;
|
||||
const restAt = sig.params.findIndex((shape) => shape.mode === "rest");
|
||||
if (restAt >= 0 && i >= restAt) {
|
||||
// Rest always supplies an array. An absent source value
|
||||
// widens its elements, never the array parameter itself.
|
||||
if (ts.isSpreadElement(arg) || !mayBeOptional(arg) || !shape) return;
|
||||
if (i === restAt) {
|
||||
// A rest binding always receives an array. An unchecked
|
||||
// argument can make its elements undefined, not the pack.
|
||||
// Keep every virtual implementation on the same array ABI.
|
||||
for (const target of familyBySymbol.get(symbol) ?? [symbol]) {
|
||||
const shape = signatureBySymbol.get(target)?.params[restAt];
|
||||
if (shape?.type.kind !== "array") continue;
|
||||
const element = addUndefined(shape.type.elem);
|
||||
if (typeEquals(element, shape.type.elem)) continue;
|
||||
shape.type = arrayOf(element);
|
||||
const parameter = functionDeclBySymbol.get(target)?.parameters[restAt];
|
||||
const bound = parameter && ts.isIdentifier(parameter.name) ? symbolOf(parameter.name) : null;
|
||||
if (bound) this.runtimeOptionalBindingTypes.set(bound, shape.type);
|
||||
const rest = signatureBySymbol.get(target)?.params[i];
|
||||
if (rest?.mode !== "rest" || rest.type.kind !== "array") continue;
|
||||
const widened = arrayOf(addUndefined(rest.type.elem));
|
||||
if (typeEquals(rest.type, widened)) continue;
|
||||
rest.type = widened;
|
||||
const parameter = functionDeclBySymbol.get(target)?.parameters[i];
|
||||
if (parameter && ts.isIdentifier(parameter.name)) {
|
||||
const bound = symbolOf(parameter.name);
|
||||
if (bound) this.runtimeOptionalBindingTypes.set(bound, widened);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!sig.params[i]) return;
|
||||
const set = optionalParams.get(symbol) ?? new Set<number>();
|
||||
sig.params[i]!.type = addUndefined(sig.params[i]!.type);
|
||||
shape.type = addUndefined(shape.type);
|
||||
const before = set.size;
|
||||
set.add(i);
|
||||
optionalParams.set(symbol, set);
|
||||
@@ -3836,9 +3845,6 @@ export class Lowerer {
|
||||
// slots), uniformly across params/locals/globals/fields/body types so
|
||||
// every producer and consumer agrees. Programs with no unregistered
|
||||
// reference are untouched — byte-stability holds.
|
||||
if (this.diags.length === 0) {
|
||||
this.sanitizeUnregisteredClassTypes([functions, this.globalsList, artifacts.classes, artifacts.records, artifacts.unions]);
|
||||
}
|
||||
const module: IrModule | null =
|
||||
this.diags.length > 0
|
||||
? null
|
||||
@@ -3854,6 +3860,7 @@ export class Lowerer {
|
||||
entry: ENTRY_NAME,
|
||||
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
|
||||
};
|
||||
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
|
||||
return {
|
||||
module,
|
||||
diagnostics: this.diags,
|
||||
@@ -3866,40 +3873,6 @@ export class Lowerer {
|
||||
};
|
||||
}
|
||||
|
||||
/** The unregistered-class type sweep (run()'s last step before the
|
||||
* module assembles): every `{kind:"object"}` TYPE naming a class with
|
||||
* no registered ClassInfo is rewritten IN PLACE to the f64 dummy.
|
||||
* classval types are exempt (they emit the class-independent
|
||||
* `ScrClassObj *` — inert-but-valid storage, the validator's own
|
||||
* stance), and only type objects rewrite — node-level classNames
|
||||
* (`new`, upcasts) cannot reach here (their lowerings fence without a
|
||||
* registered class), so the validator still backstops those. */
|
||||
sanitizeUnregisteredClassTypes(roots: unknown[]): void {
|
||||
const isUnregisteredObjectType = (v: unknown): boolean =>
|
||||
typeof v === "object" && v !== null &&
|
||||
(v as { kind?: unknown }).kind === "object" &&
|
||||
typeof (v as { className?: unknown }).className === "string" &&
|
||||
!this.classes.has((v as { className: string }).className);
|
||||
const sweep = (node: unknown): void => {
|
||||
if (node === null || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((item, i) => {
|
||||
if (isUnregisteredObjectType(item)) node[i] = F64;
|
||||
else sweep(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const rec = node as Record<string, unknown>;
|
||||
for (const key of Object.keys(rec)) {
|
||||
if (key === "loc") continue;
|
||||
const v = rec[key];
|
||||
if (isUnregisteredObjectType(v)) rec[key] = F64;
|
||||
else sweep(v);
|
||||
}
|
||||
};
|
||||
for (const root of roots) sweep(root);
|
||||
}
|
||||
|
||||
/** True when `t` (recursively) names a class instance type with no
|
||||
* registered ClassInfo — the shape of a JS class whose collection fenced.
|
||||
* Used by run()'s global pruning; shapes/unions recurse with a seen-set
|
||||
@@ -4313,7 +4286,7 @@ export class Lowerer {
|
||||
// turn queue more instances. Continue to the joint fixpoint; the initial
|
||||
// queue above is the only portion whose discovery order differed from
|
||||
// historical emit order.
|
||||
const classDispatch = new ClassDynamicDispatch();
|
||||
const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined);
|
||||
for (;;) {
|
||||
drainInstances();
|
||||
const dispatchChanged = classDispatch.process(this, [
|
||||
@@ -5355,6 +5328,12 @@ export class Lowerer {
|
||||
return { kind: "promiseVoidWiden", value: expr, type: expected, loc: expr.loc };
|
||||
}
|
||||
if (expected.kind === "dyn" && expr.type.kind !== "dyn") {
|
||||
// Native void calls still have a JavaScript value: undefined. Keep
|
||||
// the call's effects before exposing that value to checked JS code.
|
||||
if (expr.type.kind === "void") {
|
||||
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
|
||||
result: dynUndefinedExpr(expr.loc), type: DYN, loc: expr.loc };
|
||||
}
|
||||
// An error-HIERARCHY object (builtin subclass or user `extends
|
||||
// Error` class) upcasts to the %Error root first — the caughtToDyn
|
||||
// encoding (scr_dyn_from_error) carries name/message/code and the
|
||||
@@ -5795,6 +5774,7 @@ export class Lowerer {
|
||||
const sameFamily =
|
||||
(src.kind === "record" && arm.kind === "record") ||
|
||||
(src.kind === "array" && arm.kind === "array") ||
|
||||
(src.kind === "object" && arm.kind === "object") ||
|
||||
// Tuples already lift into ordinary array slots. Consider that
|
||||
// same conversion when the array is an arm of a union too.
|
||||
(src.kind === "record" && this.shapes.get(src.shapeId)?.tuple === true && arm.kind === "array") ||
|
||||
@@ -7824,6 +7804,17 @@ export class Lowerer {
|
||||
* lowering goes through here (via lowerExprExpecting) or calls this
|
||||
* directly when the expression was already lowered. */
|
||||
coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr {
|
||||
// A union destination can accept the binding's tagged storage directly.
|
||||
// The checker's single-arm flow type can be stale after a callback writes
|
||||
// the binding. Extracting that arm and wrapping it again would discard the
|
||||
// actual tag and could read a different record layout. Retag the stored
|
||||
// value instead; ordinary coercion checks any excluded destination arms.
|
||||
// Explicit assertions keep their own conversion and validation.
|
||||
if (expected.kind === "union" && expr.kind === "unionNarrow") {
|
||||
let source = node;
|
||||
while (ts.isParenthesizedExpression(source) || ts.isSatisfiesExpression(source)) source = source.expression;
|
||||
if (ts.isIdentifier(source) || ts.isShorthandPropertyAssignment(source)) expr = expr.value;
|
||||
}
|
||||
// A fresh literal can retain a wider runtime-optional field after its
|
||||
// initial contextual layout was chosen. Its known discriminator still
|
||||
// selects the destination arm; validate that payload before wrapping.
|
||||
@@ -7859,6 +7850,15 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
let e = this.coerceToExpected(expr, expected);
|
||||
// Existing JavaScript arrays retain mutations and identity across checked
|
||||
// slots. Fresh literals have no prior identity and use checked storage
|
||||
// directly, so later writes can change their inferred element type.
|
||||
let literal = node;
|
||||
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
|
||||
if (isJsSourceFile(node.getSourceFile()) && !ts.isArrayLiteralExpression(literal) &&
|
||||
e.kind === "dynFrom" && e.value.type.kind === "array") {
|
||||
e = { ...e, liveRef: true };
|
||||
}
|
||||
// An 'any' value PROVABLY null/undefined (the unit literal itself, or
|
||||
// a read of a binding nothing ever assigns a non-unit value) flowing
|
||||
// implicitly into a primitive slot: the validated exit refuses units
|
||||
@@ -8170,7 +8170,12 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
}
|
||||
const e = this.lowerExpr(node);
|
||||
let e = this.lowerExpr(node);
|
||||
if (expected?.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) {
|
||||
// A destination that accepts undefined must retain an unchecked
|
||||
// read's storage value even when the checker still calls it present.
|
||||
e = this.runtimeOptionalSourceValue(node, e) ?? e;
|
||||
}
|
||||
return expected ? this.coerceInto(node, e, expected) : e;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc, isUnitType, typeEquals } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Array/property reads retain a missing-value arm even when the checker
|
||||
* promises a record. Object enumeration must check it before reading fields. */
|
||||
export function objectEnumerationReceiver(
|
||||
lowerer: Lowerer, receiver: IrExpr, expected: IrType & { kind: "record" }, loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (receiver.type.kind !== "union") return receiver;
|
||||
const source = receiver.type;
|
||||
const arms = lowerer.unions.get(source.unionId)?.arms;
|
||||
const tag = lowerer.armTag(source.unionId, expected);
|
||||
if (tag < 0 || !arms?.every((arm) => isUnitType(arm) || typeEquals(arm, expected))) return receiver;
|
||||
const key = `obj.receiver:${source.unionId}:${expected.shapeId}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = `%obj.receiver.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const value = varRef("value.0", source, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name, params: [{ localId: "value.0", name: "value", type: source }],
|
||||
returnType: expected, locals: [{ id: "value.0", name: "value", type: source, mutable: false }],
|
||||
body: [{
|
||||
kind: "if", cond: { kind: "unionIsTag", unionId: source.unionId, tag, value, negated: false, type: BOOL, loc },
|
||||
then: [{ kind: "return", value: { kind: "unionNarrow", unionId: source.unionId, tag, value, type: expected, loc }, loc }],
|
||||
else_: [{ kind: "throw", value: { kind: "libCall", fn: "error.new",
|
||||
args: [{ kind: "strLit", value: "Cannot convert undefined or null to object", type: STRING, loc }],
|
||||
type: { kind: "object", className: "%TypeError" }, loc }, loc }], loc,
|
||||
}], loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type: expected, loc };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user