mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Make LLVM the sole production backend
- Remove C emission, debugging options, and automatic backend fallback. - Complete LLVM lowering and native bootstrap coverage across supported targets. - Link executables and libraries with precompiled C runtime packs, including iOS and Android.
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
!packages/
|
||||
!packages/cli/
|
||||
!packages/cli/package.json
|
||||
!packages/cli/scripts/
|
||||
!packages/cli/scripts/warm-cache.mjs
|
||||
!packages/compiler/
|
||||
!packages/compiler/package.json
|
||||
!packages/runtime/
|
||||
|
||||
+76
-19
@@ -5,6 +5,10 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: test (${{ matrix.flavor }}, ${{ matrix.shard }}/10)
|
||||
@@ -64,6 +68,7 @@ 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
|
||||
@@ -83,12 +88,6 @@ jobs:
|
||||
env:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
run: pnpm test tests/harness/self-hosting-llvm-emitter.test.ts
|
||||
- name: Tests (native compiler bootstrap)
|
||||
if: matrix.shard == 6
|
||||
env:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
VITEST_RPC_TIMEOUT: "180000"
|
||||
run: pnpm test tests/harness/self-hosting-native-driver.test.ts
|
||||
# These long files otherwise collide in one Vitest file shard and contend
|
||||
# for the worker RPC. Spread them across jobs with one worker each.
|
||||
- name: Tests (fetch)
|
||||
@@ -117,6 +116,10 @@ jobs:
|
||||
SCRIPTC_TEST_STABLE_TOOLCHAIN: "0"
|
||||
run: pnpm test packages/compiler/src/backend/native-toolchain.test.ts
|
||||
- name: Tests (case-sharded, slice ${{ matrix.shard }}/10)
|
||||
env:
|
||||
# differential.test.ts owns release; this companion adds dev.
|
||||
# Packaged-artifact jobs still exercise both optimization modes.
|
||||
SCRIPTC_LLVM_TEST_MODE: dev
|
||||
run: >-
|
||||
pnpm test
|
||||
tests/harness/differential.test.ts
|
||||
@@ -131,6 +134,53 @@ jobs:
|
||||
if: matrix.flavor == 'plain'
|
||||
run: pnpm test tests/harness/coverage.test.ts
|
||||
|
||||
# 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)
|
||||
# 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
|
||||
strategy:
|
||||
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' || '' }}
|
||||
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
|
||||
- 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"
|
||||
- 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
|
||||
- run: pnpm build
|
||||
- run: pnpm test ${{ matrix.test }}
|
||||
|
||||
# A real host-clang/glibc build, not the zig cross-target lane: catches
|
||||
# Linux feature-test macro omissions at compile time and separate-libm
|
||||
# omissions/order errors at link time.
|
||||
@@ -292,16 +342,8 @@ jobs:
|
||||
packages/cli/test/native-link-info.test.ts
|
||||
packages/cli/test/runtime-pack.test.ts
|
||||
tests/harness/native-object-example.test.ts
|
||||
- name: Default helper/runtime-pack executable does not compile C
|
||||
if: matrix.shard == 1
|
||||
- name: LLVM release/dev differential (${{ matrix.shard }}/5)
|
||||
env:
|
||||
SCRIPTC_LEGACY_C_PIPELINE: "0"
|
||||
run: >-
|
||||
pnpm test packages/cli/test/runtime-pack.test.ts
|
||||
--testNamePattern "legacy C pipeline disabled"
|
||||
- name: LLVM-tier helper object differential (${{ matrix.shard }}/5)
|
||||
env:
|
||||
SCRIPTC_LLVM_HELPER_ONLY: "1"
|
||||
SCRIPTC_TEST_WORKERS: "4"
|
||||
run: pnpm test tests/harness/llvm-differential.test.ts
|
||||
- name: Packaged helper contract
|
||||
@@ -370,6 +412,7 @@ jobs:
|
||||
# slash styles, and the default executable must use the .exe suffix.
|
||||
windows_cli:
|
||||
name: test (windows CLI smoke)
|
||||
needs: llvm_artifacts_windows_x64
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
@@ -396,6 +439,10 @@ jobs:
|
||||
Write-Host "pnpm exited without a diagnostic; retrying once"
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-native-packages
|
||||
path: packages
|
||||
- run: pnpm build
|
||||
- name: Windows path regressions
|
||||
run: pnpm exec vitest run packages/compiler/test/ts7/program-adapter.test.ts packages/compiler/test/ts7/resolver-parity.test.ts packages/cli/test/paths.test.ts
|
||||
@@ -407,7 +454,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
zig version
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend c
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
@@ -426,7 +473,7 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$nativeOutput = & node packages/cli/dist/main.js run tests/corpus/2557-tls-ca-store.ts --backend c
|
||||
$nativeOutput = & node packages/cli/dist/main.js run tests/corpus/2557-tls-ca-store.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
@@ -495,17 +542,27 @@ jobs:
|
||||
node packages/cli/dist/main.js build tests/corpus/001-hello.ts --emit=obj -o "$env:RUNNER_TEMP\hello.obj"
|
||||
$bytes = [System.IO.File]::ReadAllBytes("$env:RUNNER_TEMP\hello.obj")
|
||||
if ($bytes[0] -ne 0x64 -or $bytes[1] -ne 0x86) { throw "expected AMD64 COFF object" }
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-native-packages
|
||||
path: |
|
||||
packages/llvm-win32-x64-msvc/bin
|
||||
packages/runtime-win32-x64-msvc/artifacts
|
||||
packages/runtime-win32-x64-msvc/runtime-pack.json
|
||||
retention-days: 1
|
||||
|
||||
# Retain the pre-matrix "test" check for branch protection and badges. It
|
||||
# passes only when every matrix shard and independent integration job does.
|
||||
test:
|
||||
needs: [tests, linux_host_clang, ts7_parity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64]
|
||||
if: always()
|
||||
needs: [tests, bootstrap, linux_host_clang, ts7_parity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64]
|
||||
# A superseded run must release its concurrency slot immediately.
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: All lanes green
|
||||
run: |
|
||||
test "${{ needs.tests.result }}" = success
|
||||
test "${{ needs.bootstrap.result }}" = success
|
||||
test "${{ needs.linux_host_clang.result }}" = success
|
||||
test "${{ needs.ts7_parity.result }}" = success
|
||||
test "${{ needs.llvm_artifacts_linux_x64.result }}" = success
|
||||
|
||||
@@ -179,9 +179,47 @@ jobs:
|
||||
packages/${{ matrix.runtime }}
|
||||
retention-days: 1
|
||||
|
||||
build-mobile-runtime-packs:
|
||||
name: Build ${{ matrix.runtime }}
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_release == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runtime: runtime-ios-arm64
|
||||
runner: macos-15
|
||||
- runtime: runtime-ios-simulator-arm64
|
||||
runner: macos-15
|
||||
- runtime: runtime-android-arm64
|
||||
runner: ubuntu-24.04
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.15.0
|
||||
- name: Install Android NDK
|
||||
if: matrix.runtime == 'runtime-android-arm64'
|
||||
run: |
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;27.2.12479018"
|
||||
echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/27.2.12479018" >> "$GITHUB_ENV"
|
||||
- name: Build and verify mobile runtime pack
|
||||
run: |
|
||||
node packages/${{ matrix.runtime }}/scripts/build.mjs
|
||||
node packages/${{ matrix.runtime }}/scripts/verify.mjs
|
||||
mkdir -p "$RUNNER_TEMP/mobile-pack/${{ matrix.runtime }}"
|
||||
cp -R packages/${{ matrix.runtime }}/. "$RUNNER_TEMP/mobile-pack/${{ matrix.runtime }}/"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-${{ matrix.runtime }}
|
||||
path: ${{ runner.temp }}/mobile-pack
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
name: Publish to npm
|
||||
needs: [check-release, build-native-packages]
|
||||
needs: [check-release, build-native-packages, build-mobile-runtime-packs]
|
||||
if: needs.check-release.outputs.should_release == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 15
|
||||
@@ -233,7 +271,7 @@ jobs:
|
||||
- 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-win32-x64-msvc packages/llvm-win32-x64-msvc packages/compiler packages/cli; do
|
||||
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
|
||||
V=$(node -p "require('./$pkg/package.json').version")
|
||||
if [ "$V" != "$VERSION" ]; then
|
||||
echo "Version mismatch: $pkg is $V, expected $VERSION"
|
||||
@@ -326,6 +364,9 @@ jobs:
|
||||
publish_dir packages/llvm-linux-x64-musl
|
||||
publish_dir packages/runtime-linux-arm64-musl
|
||||
publish_dir packages/llvm-linux-arm64-musl
|
||||
publish_dir packages/runtime-ios-arm64
|
||||
publish_dir packages/runtime-ios-simulator-arm64
|
||||
publish_dir packages/runtime-android-arm64
|
||||
publish_dir packages/runtime-wasm32-wasi
|
||||
publish_dir packages/runtime-win32-x64-msvc
|
||||
publish_dir packages/llvm-win32-x64-msvc
|
||||
|
||||
@@ -56,8 +56,8 @@ new tests should follow this convention.
|
||||
|
||||
## Where things live
|
||||
|
||||
- `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM and C backends.
|
||||
- `packages/runtime` — the C runtime compiled into every scriptc binary.
|
||||
- `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM backend.
|
||||
- `packages/runtime` — the C runtime distributed as precompiled runtime packs.
|
||||
- `packages/cli` — `scriptc build | run | coverage`.
|
||||
- `internal/compatibility` — generated Node.js parity inventory, implementation-owned compatibility manifests, and engineering backlog.
|
||||
- `tests/` — the differential corpus, diagnostics snapshots, and harness.
|
||||
|
||||
@@ -56,8 +56,6 @@ WORKDIR /workspace
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY packages/cli/package.json packages/cli/package.json
|
||||
COPY packages/cli/scripts/warm-cache.mjs packages/cli/scripts/warm-cache.mjs
|
||||
COPY packages/cli/scripts/runtime-pack-host.mjs packages/cli/scripts/runtime-pack-host.mjs
|
||||
COPY packages/compiler/package.json packages/compiler/package.json
|
||||
COPY packages/runtime/package.json packages/runtime/package.json
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<a href="https://www.npmjs.com/package/scriptc"><img alt="npm downloads per month: scriptc" src="https://img.shields.io/npm/dm/scriptc.svg?style=for-the-badge&labelColor=000000&label=npm%20downloads" height="28"></a>
|
||||
</p>
|
||||
|
||||
scriptc compiles TypeScript and JavaScript to typed IR, readable C, textual LLVM IR, native assembly and objects, native executables, and WebAssembly modules. It uses the TypeScript compiler for parsing and type checking. Source outputs require only Node. On macOS 15+ arm64, ordinary LLVM-tier executables use scriptc's bundled helper and precompiled runtime pack; clang is only the platform linker driver and does not compile program or runtime C.
|
||||
scriptc compiles TypeScript and JavaScript to typed IR, textual LLVM IR, native assembly and objects, native executables, and WebAssembly modules. It uses the TypeScript compiler for parsing and type checking. Source outputs require only Node. On macOS 15+ arm64, ordinary LLVM executables use scriptc's bundled helper and precompiled runtime pack; clang is only the platform linker driver and does not compile program or runtime C.
|
||||
|
||||
Static builds include a small native runtime, but no Node or JavaScript engine. Code that cannot compile statically is reported as a diagnostic. For npm packages and `any`-typed code, `--dynamic` embeds [quickjs-ng](https://github.com/quickjs-ng/quickjs) explicitly.
|
||||
|
||||
@@ -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|c|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-tier 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. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route additionally need a C compiler. The executables it produces do not require Node.
|
||||
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.
|
||||
|
||||
```console
|
||||
$ npm install -g scriptc
|
||||
@@ -51,24 +51,17 @@ Or stop at a source-level compiler artifact without invoking clang, an archiver,
|
||||
$ scriptc build hello.ts --emit=ir >/dev/null
|
||||
$ ls .scriptc/
|
||||
hello.ir.json
|
||||
$ scriptc build hello.ts --emit=c >/dev/null
|
||||
$ ls .scriptc/
|
||||
hello.c
|
||||
hello.ir.json
|
||||
$ scriptc build hello.ts --emit=llvm >/dev/null
|
||||
$ ls .scriptc/
|
||||
hello.c
|
||||
hello.ir.json
|
||||
hello.ll
|
||||
$ scriptc build hello.ts --emit=asm >/dev/null
|
||||
$ ls .scriptc/
|
||||
hello.c
|
||||
hello.ir.json
|
||||
hello.ll
|
||||
hello.s
|
||||
$ scriptc build hello.ts --emit=obj >/dev/null
|
||||
$ ls .scriptc/
|
||||
hello.c
|
||||
hello.ir.json
|
||||
hello.ll
|
||||
hello.o
|
||||
@@ -77,14 +70,9 @@ hello.s
|
||||
|
||||
Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.
|
||||
|
||||
`--emit=obj` writes a relocatable program object, not a standalone library. It has undefined `scr_*` runtime references and a required `scr_runtime_abi_v3` marker; `scriptc build --lib --profile ...` remains the self-contained archive interface. The helper runs on macOS 15+ arm64 and emits artifacts with an `arm64-apple-macosx14.0.0` deployment target. Sanitized assembly/object emission is rejected until the helper's AddressSanitizer pipeline matches the executable path.
|
||||
`--emit=obj` writes a relocatable program object, not a standalone library. It has undefined `scr_*` runtime references and a required `scr_runtime_abi_v4` marker; `scriptc build --lib --profile ...` remains the self-contained archive interface. The helper runs on macOS 15+ arm64 and emits artifacts with an `arm64-apple-macosx14.0.0` deployment target. Sanitized assembly/object emission is rejected until the helper's AddressSanitizer pipeline matches the executable path.
|
||||
|
||||
External object consumption is experimental. Use
|
||||
`--print=native-link-info` to emit the object and print a versioned JSON recipe
|
||||
containing its target, `main` entry, exact `@scriptc/runtime` source pack,
|
||||
required system libraries, FFI inputs, and ABI marker. The recipe never uses
|
||||
hidden scriptc cache paths. See [`examples/native-object`](./examples/native-object)
|
||||
for C-driver and direct Apple-linker builds.
|
||||
External object consumption is experimental. Use `--print=native-link-info` to emit the object and print a versioned JSON recipe containing its target, `main` entry, exact precompiled runtime pack, required system libraries, FFI inputs, and ABI marker. The recipe never uses hidden scriptc cache paths. See [`examples/native-object`](./examples/native-object) for C-driver and direct Apple-linker builds.
|
||||
|
||||
## Use Node APIs
|
||||
|
||||
@@ -126,13 +114,13 @@ $ scriptc coverage hello.ts
|
||||
|
||||
WASI and other cross-target builds require Zig. Its bundled WASI libc produces a portable WASI Preview 1 module through the production LLVM backend:
|
||||
|
||||
Install Zig and make sure the `zig` executable is available on your `PATH`. `SCRIPTC_CC=zigcc` is scriptc's selector for invoking Zig's `cc` subcommand; `zigcc` is not a standalone executable.
|
||||
Install Zig and make sure the `zig` executable is available on your `PATH`. scriptc uses it to link the program object with the precompiled WASI runtime pack and WASI libc.
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=wasm32-wasi scriptc build hello.ts --no-keep-c -o hello.wasm >/dev/null
|
||||
$ SCRIPTC_TARGET=wasm32-wasi scriptc build hello.ts --no-keep-llvm -o hello.wasm >/dev/null
|
||||
$ file hello.wasm
|
||||
hello.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=wasm32-wasi scriptc run hello.ts
|
||||
$ SCRIPTC_TARGET=wasm32-wasi scriptc run hello.ts
|
||||
hello, world
|
||||
```
|
||||
|
||||
@@ -167,28 +155,8 @@ $ vercel link && vercel env pull # writes a project-scoped VERCEL_OIDC_TOKEN
|
||||
$ pnpm test:sandbox
|
||||
```
|
||||
|
||||
The normal workspace build needs no local LLVM installation. To rebuild a
|
||||
native helper/runtime pack, install CMake, Ninja, and the pinned LLVM 22
|
||||
development package on that target host, then run the matching
|
||||
`@scriptc/llvm-<platform>` and `@scriptc/runtime-<platform>` `build:native`
|
||||
scripts. The macOS full test suite also uses those generated artifacts.
|
||||
The normal workspace build needs no local LLVM installation. To rebuild a native helper/runtime pack, install CMake, Ninja, and the pinned LLVM 22 development package on that target host, then run the matching `@scriptc/llvm-<platform>` and `@scriptc/runtime-<platform>` `build:native` scripts. The macOS full test suite also uses those generated artifacts.
|
||||
|
||||
`pnpm test:sandbox` loads `.env.local`, preflights Vercel authentication and
|
||||
project access, and uses the managed `vercel/sandbox/universal` image by
|
||||
default. It installs the repository-pinned Node, pnpm, and LLVM toolchain plus
|
||||
ScriptC dependencies in each disposable Sandbox before building the uploaded
|
||||
worktree. Set
|
||||
`SCRIPTC_SANDBOX_IMAGE` to a fully qualified VCR reference only to use the
|
||||
optional prebuilt image from `pnpm test:sandbox:image`.
|
||||
The prebuilt image keeps the roughly four-minute fast path; cold managed-image
|
||||
runs take longer because they install the pinned toolchain in each Sandbox.
|
||||
`pnpm test:sandbox` loads `.env.local`, preflights Vercel authentication and project access, and uses the managed `vercel/sandbox/universal` image by default. It installs the repository-pinned Node, pnpm, and LLVM toolchain plus scriptc dependencies in each disposable Sandbox before building the uploaded worktree. Set `SCRIPTC_SANDBOX_IMAGE` to a fully qualified VCR reference only to use the optional prebuilt image from `pnpm test:sandbox:image`. The prebuilt image keeps the roughly four-minute fast path; cold managed-image runs take longer because they install the pinned toolchain in each Sandbox.
|
||||
|
||||
`VERCEL_OIDC_TOKEN` is preferred. For access-token authentication, set
|
||||
`VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID`; team and project are
|
||||
never inferred from `SCRIPTC_SANDBOX_IMAGE`. The legacy VCR command used by
|
||||
`pnpm test:sandbox:image` cannot authenticate with an OIDC JWT, so image builds
|
||||
use `VERCEL_TOKEN` when available or the existing Vercel CLI login; OIDC claims
|
||||
still select the VCR team and project. The test corpus runs each program
|
||||
under Node and as a compiled native binary, then compares stdout, stderr, and
|
||||
exit codes byte for byte. The full gate also runs the corpus with
|
||||
AddressSanitizer and the runtime reference-count audit.
|
||||
`VERCEL_OIDC_TOKEN` is preferred. For access-token authentication, set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID`; team and project are never inferred from `SCRIPTC_SANDBOX_IMAGE`. The legacy VCR command used by `pnpm test:sandbox:image` cannot authenticate with an OIDC JWT, so image builds use `VERCEL_TOKEN` when available or the existing Vercel CLI login; OIDC claims still select the VCR team and project. The test corpus runs each program under Node and as a compiled native binary, then checks that stdout, stderr, and exit codes match exactly. The full gate also runs the corpus with AddressSanitizer and the runtime reference-count audit.
|
||||
|
||||
+26
-54
@@ -13,13 +13,13 @@ Usage:
|
||||
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 cache warm [runtime|tls|dynamic…] prebuild expensive native cache families
|
||||
scriptc cache warm [runtime|tls|dynamic…] verify and load precompiled runtime families
|
||||
for the current compiler/SDK/target
|
||||
```
|
||||
|
||||
## scriptc build
|
||||
|
||||
Compiles a TypeScript (or JavaScript) entry file to serialized typed IR, readable C, textual LLVM IR, native assembly, a relocatable object, a native executable, or a WebAssembly module when the <code>wasm32-wasi</code> target is selected. The program is type-checked first — by the real TypeScript compiler, honoring the nearest `tsconfig.json` — and any construct without a lowering is a compile error with an `SC`-prefixed code, a code frame, and usually a rewrite hint.
|
||||
Compiles a TypeScript (or JavaScript) entry file to serialized typed IR, textual LLVM IR, native assembly, a relocatable object, a native executable, or a WebAssembly module when the <code>wasm32-wasi</code> target is selected. The program is type-checked first — by the real TypeScript compiler, honoring the nearest `tsconfig.json` — and any construct without a lowering is a compile error with an `SC`-prefixed code, a code frame, and usually a rewrite hint.
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts -o fib
|
||||
@@ -33,24 +33,17 @@ Without `-o`, the primary artifact lands in `.scriptc/` next to the input. Sourc
|
||||
$ scriptc build fib.ts --emit=ir >/dev/null
|
||||
$ ls .scriptc/
|
||||
fib.ir.json
|
||||
$ scriptc build fib.ts --emit=c >/dev/null
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
$ scriptc build fib.ts --emit=llvm >/dev/null
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
$ scriptc build fib.ts --emit=asm >/dev/null
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
fib.s
|
||||
$ scriptc build fib.ts --emit=obj >/dev/null
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
fib.o
|
||||
@@ -59,7 +52,7 @@ fib.s
|
||||
|
||||
Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.
|
||||
|
||||
The three source outputs require only Node. On macOS 15+ arm64, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v4</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed source runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. On macOS 15+ arm64, LLVM-tier builds emit the program object through the helper and link release-built runtime objects; explicit C, LLVM fallback, and sanitizer builds retain runtime C compilation.
|
||||
The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v4</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime.
|
||||
|
||||
## scriptc run
|
||||
|
||||
@@ -78,19 +71,19 @@ Analyzes the program without producing a binary and reports, statement by statem
|
||||
|
||||
## scriptc cache warm
|
||||
|
||||
Prebuilds release runtime objects and native TLS/dynamic-engine archives for targets that still compile runtime C locally. macOS 15+ arm64 installations already carry the release-built runtime pack and skip automatic warming; older macOS hosts retain warming for the source-toolchain path. Use this command when preparing another supported target's container image or CI runner. Pass one or more of `runtime`, `tls`, and `dynamic` to seed only those families.
|
||||
Verifies and reads the installed precompiled runtime objects and vendor archives. Pass one or more of `runtime`, `tls`, and `dynamic` to select those families. Ordinary builds need no runtime compilation or installation-time warming. With `--sanitize`, this command builds instrumented runtime objects using the development C toolchain.
|
||||
|
||||
## Options
|
||||
|
||||
<dl>
|
||||
<dt><code>-o, --out <path></code></dt>
|
||||
<dd>Primary artifact path. An explicit path is exact. Defaults are <code>.scriptc/<name>.ir.json</code>, <code>.c</code>, <code>.ll</code>, <code>.s</code>, <code>.o</code>, or the platform executable name.</dd>
|
||||
<dd>Primary artifact path. An explicit path is exact. Defaults are <code>.scriptc/<name>.ir.json</code>, <code>.ll</code>, <code>.s</code>, <code>.o</code>, or the platform executable name.</dd>
|
||||
|
||||
<dt><code>--emit <ir|c|llvm|asm|obj|exe></code></dt>
|
||||
<dd>Select the invocation's one primary artifact. <code>ir</code>, <code>c</code>, and <code>llvm</code> need only Node. <code>asm</code> and <code>obj</code> use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. <code>exe</code> is the default.</dd>
|
||||
<dt><code>--emit <ir|llvm|asm|obj|exe></code></dt>
|
||||
<dd>Select the invocation's one primary artifact. <code>ir</code> and <code>llvm</code> need only Node. <code>asm</code> and <code>obj</code> use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. <code>exe</code> is the default.</dd>
|
||||
|
||||
<dt><code>--print <native-link-info></code></dt>
|
||||
<dd>Build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed source runtime pack and all link inputs, but does not invoke a linker.</dd>
|
||||
<dd>Build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker.</dd>
|
||||
|
||||
<dt><code>--dynamic</code></dt>
|
||||
<dd>Embed the dynamic engine (~620KB) so npm dependencies and <code>any</code>-typed code can run. Static stays the default — without this flag, dynamic-tier sites are per-site compile errors. See <a href="/dependencies">npm Dependencies</a>.</dd>
|
||||
@@ -98,17 +91,17 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
|
||||
<dt><code>--ffi <file></code></dt>
|
||||
<dd>Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See <a href="/ffi">Native FFI</a>.</dd>
|
||||
|
||||
<dt><code>--backend <c|llvm></code></dt>
|
||||
<dd>Code generator: <code>llvm</code> (default) or <code>c</code> (the readable debugging backend). On supported targets, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>.</dd>
|
||||
<dt><code>--backend <llvm></code></dt>
|
||||
<dd>The production code generator is <code>llvm</code>. This is also the default when the option is omitted.</dd>
|
||||
|
||||
<dt><code>--strip</code></dt>
|
||||
<dd>Remove symbol and debug payload when linking an executable, including cross-compiled targets. This opt-in flag works with either backend and <code>--from-c</code> without changing code optimization.</dd>
|
||||
<dd>Remove symbol and debug payload when linking an executable, including cross-compiled targets. This flag preserves code optimization.</dd>
|
||||
|
||||
<dt><code>--optimization <release|dev></code></dt>
|
||||
<dd>Select native optimization for executable, assembly, or object output. The default is <code>release</code> (<code>-O2</code>). <code>dev</code> uses <code>-O0</code> and emits source locations for TypeScript and JavaScript breakpoints and stack frames. LLVM builds include source variable names and native storage descriptions; see <a href="/limitations#native-debugging">Native debugging</a> for the supported representations. macOS executable builds also produce an adjacent <code>.dSYM</code> bundle.</dd>
|
||||
|
||||
<dt><code>--windows-subsystem <console|gui></code></dt>
|
||||
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows, including <code>--from-c</code>.</dd>
|
||||
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>
|
||||
@@ -125,11 +118,8 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
|
||||
<dt><code>--emit-ir</code></dt>
|
||||
<dd>Additive IR side artifact. It is deprecated for executable builds for one release cycle; prefer <code>--emit=ir</code> when IR is the primary output. Library mode retains <code>--emit-ir</code> because <code>--emit</code> does not select library artifacts.</dd>
|
||||
|
||||
<dt><code>--keep-c</code> / <code>--no-keep-c</code></dt>
|
||||
<dd>Keep (default) or delete the generated program translation unit next to the executable — the <code>.ll</code> file under the default LLVM backend, or the <code>.c</code> under <code>--backend c</code> (and when the default build fell back). The generated C is readable and annotated with source lines.</dd>
|
||||
|
||||
<dt><code>--from-c</code></dt>
|
||||
<dd>Treat the input as a C (or <code>.ll</code>) file. Toolchain plumbing and debugging. These arbitrary translation units bypass the persistent artifact cache because their header dependency graph is caller-owned.</dd>
|
||||
<dt><code>--keep-llvm</code> / <code>--no-keep-llvm</code></dt>
|
||||
<dd>Keep (default) or delete the generated <code>.ll</code> file next to the executable.</dd>
|
||||
|
||||
<dt><code>-h, --help</code></dt>
|
||||
<dd>Show usage.</dd>
|
||||
@@ -139,7 +129,7 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
|
||||
|
||||
<dl>
|
||||
<dt><code>SCRIPTC_CACHE_DIR</code></dt>
|
||||
<dd>Override the persistent build-cache root. By default it is <code>$XDG_CACHE_HOME/scriptc/build</code> when that variable is set, <code>$HOME/Library/Caches/scriptc/build</code> on macOS, <code>%LOCALAPPDATA%\scriptc\cache\build</code> on Windows, and <code>$HOME/.cache/scriptc/build</code> on other platforms. The cache stores checksum-verified, content-addressed executables, library archives, and per-flavor runtime objects. Identities include the compiler's resolved system-header dependencies and linker/assembler identities. The compiler remains required so dependency selection is rediscovered on every cache-enabled invocation. FFI builds with archive/object inputs or ambient <code>system_libraries</code> always relink against their current dependencies while retaining runtime-object reuse. Mutable compiler input paths such as <code>CPATH</code> and <code>SDKROOT</code>, and compiler wrappers, bypass persistent artifacts and objects so dependencies rebuilt in place cannot go stale. Opaque archiver wrappers rebuild library program members and archives while retaining runtime-object reuse. Direct Clang, Apple's system Clang shim, <code>zig cc</code>, trusted platform archivers, and <code>zig ar</code> retain their applicable persistent tiers. An existing POSIX override must already be private; otherwise caching is bypassed without changing the directory's permissions.</dd>
|
||||
<dd>Override the persistent build-cache root. By default it is <code>$XDG_CACHE_HOME/scriptc/build</code> when set, <code>$HOME/Library/Caches/scriptc/build</code> on macOS, <code>%LOCALAPPDATA%\scriptc\cache\build</code> on Windows, and <code>$HOME/.cache/scriptc/build</code> elsewhere. Cached frontend results and LLVM artifacts are verified against their source, helper, target, and compilation settings. Native outputs also depend on the selected runtime pack and linker inputs. FFI objects, archives, and system libraries relink against their current dependencies. Sanitizer development builds additionally cache instrumented runtime objects and verify external compiler and header inputs. An existing POSIX override must already be private; otherwise caching is bypassed without changing the directory's permissions.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_NO_CACHE</code></dt>
|
||||
<dd>Set to <code>1</code> to bypass all build-cache reads and writes. An explicitly empty <code>SCRIPTC_CACHE_DIR</code> has the same effect.</dd>
|
||||
@@ -148,38 +138,26 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
|
||||
<dd>Maximum build-cache size in megabytes. The default is <code>4096</code>; the default cache is swept periodically, while an explicitly configured cap is checked after every successful cache write. Least-recently-used entries are removed when the cache exceeds the cap.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CC</code></dt>
|
||||
<dd>The C compiler for explicit C builds, sanitizer builds, the temporary LLVM-fallback path, and cross-compilation. <code>zigcc</code> selects zig's bundled clang and enables cross-compilation with its bundled sysroots. On macOS arm64, setting <code>SCRIPTC_CC=clang|zigcc</code> selects a deprecated legacy executable path; leave it unset for the bundled helper/runtime-pack route.</dd>
|
||||
<dd>The external compiler for runtime development with AddressSanitizer and native FFI build tooling. <code>zigcc</code> selects Zig's <code>cc</code> subcommand.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_LINKER</code></dt>
|
||||
<dd>Platform linker driver for ordinary macOS arm64 LLVM-tier executables. The driver receives only the helper-produced program object, precompiled runtime objects/archives, FFI inputs, and system libraries; it locates the platform SDK and CRT inputs but does not compile scriptc-generated or runtime C.</dd>
|
||||
<dd>Platform linker driver for ordinary LLVM executables. The driver receives only the helper-produced program object, precompiled runtime objects/archives, FFI inputs, and system libraries; it locates the platform SDK and CRT inputs but does not compile scriptc-generated or runtime C.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_TARGET</code></dt>
|
||||
<dd>Target triple for cross-compilation, e.g. <code>aarch64-linux-gnu.2.36</code>, <code>x86_64-windows-gnu</code>, or <code>wasm32-wasi</code>. WASI builds default to a <code>.wasm</code> output name. See <a href="/platforms">Platform Support</a>.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ file fib-linux
|
||||
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
|
||||
```
|
||||
|
||||
## Backends
|
||||
## LLVM backend
|
||||
|
||||
The default backend emits textual LLVM IR. On supported targets it is lowered to an object by the bundled helper and linked with the matching precompiled runtime pack; <code>SCRIPTC_LINKER</code> controls the platform linker driver, separately from C compilation. A program outside the LLVM tier is never miscompiled—the native build falls back to the C backend transparently and says so in one stderr line. The production <code>wasm32-wasi</code> target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target.
|
||||
scriptc emits LLVM IR and lowers it to native code. Executable builds link the resulting object with the matching precompiled C runtime pack. Use <code>--emit=llvm</code> to inspect generated code.
|
||||
|
||||
For source breakpoints in Xcode or LLDB, build with <code>--optimization=dev</code>. In an Xcode custom build rule, add this option to the <code>scriptc build</code> invocation and list both the executable and its <code>.dSYM</code> bundle as outputs. Keep the bundle next to the executable and retain the original source files at their build-time paths. Both the LLVM and C backends preserve source locations across imported modules.
|
||||
|
||||
The C backend is a debugging aid: deliberately readable, source-line-annotated output with differential tests against LLVM wherever the two overlap. Pin it when you want to inspect what your program became:
|
||||
|
||||
```console
|
||||
$ scriptc build fib.ts --backend c -o fib
|
||||
$ ./fib
|
||||
832040
|
||||
$ head -1 fib.c
|
||||
/* Generated by scriptc from fib.ts. Do not edit. */
|
||||
```
|
||||
|
||||
An explicit <code>--backend llvm</code> pins the LLVM backend and fails with diagnostic SC3001, naming the unsupported construct, instead of falling back — useful for CI lanes that must notice tier changes.
|
||||
For source breakpoints in Xcode or LLDB, build with <code>--optimization=dev</code>. In an Xcode custom build rule, list the executable and its <code>.dSYM</code> bundle as outputs. Keep the bundle next to the executable and retain the original source files at their build paths. LLVM preserves source locations across imported modules.
|
||||
|
||||
## Tool requirements
|
||||
|
||||
@@ -194,7 +172,7 @@ An explicit <code>--backend llvm</code> pins the LLVM backend and fails with dia
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>coverage</code>, <code>--emit=ir|c|llvm</code></td>
|
||||
<td><code>coverage</code>, <code>--emit=ir|llvm</code></td>
|
||||
<td>Required</td>
|
||||
<td>Not used</td>
|
||||
<td>Not used</td>
|
||||
@@ -206,22 +184,16 @@ An explicit <code>--backend llvm</code> pins the LLVM backend and fails with dia
|
||||
<td>Not used</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>External link of <code>--emit=obj</code> with the reported source runtime pack</td>
|
||||
<td>External link of <code>--emit=obj</code> with the reported precompiled runtime pack</td>
|
||||
<td>Not used by the artifact</td>
|
||||
<td>C compiler required for runtime sources</td>
|
||||
<td>Precompiled runtime objects</td>
|
||||
<td>macOS linker and SDK required</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--emit=exe</code></td>
|
||||
<td>Required to run scriptc</td>
|
||||
<td>clang or the selected cross compiler</td>
|
||||
<td>Required by that compiler driver</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--from-c</code></td>
|
||||
<td>Required to run scriptc</td>
|
||||
<td>Required</td>
|
||||
<td>Required</td>
|
||||
<td>Bundled LLVM helper</td>
|
||||
<td>Platform linker and SDK/sysroot</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
```
|
||||
TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ──▶ LLVM IR ──scriptc LLVM helper──▶ assembly/object
|
||||
│ │ └──precompiled runtime + linker──▶ executable
|
||||
│ └─────▶ C ────────────────C compiler/linker─────┘
|
||||
└── serialized IR
|
||||
```
|
||||
|
||||
1. **Frontend** — the real TypeScript compiler parses and type-checks your program against `es2025` (plus `@types/node` when your project has it), honoring your `tsconfig.json` for checker strictness. The frontend then lowers the checked AST into a typed intermediate representation, using tsc's own type and narrowing answers to drive every decision. A construct with no lowering is a precise diagnostic at this stage — never a miscompile later.
|
||||
2. **Typed IR** — the only interface between the ends: a validated, serializable representation (`--emit=ir` writes it as JSON and stops). Types are concrete here; generics have been monomorphized, unions are tagged values, closures have explicit captures.
|
||||
3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On supported macOS, Linux, and Windows hosts (and for WASI), `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process LLVM 22 helper; it needs no clang or linker. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production <code>wasm32-wasi</code> target never falls back.
|
||||
4. **Link** — release packages contain one precompiled object per runtime feature unit plus QuickJS, libregexp, zlib, and mbedTLS archives for every supported target. A hashed manifest maps IR feature gates to an ordered typed link plan, so binaries still pay only for what they use. The user needs the target platform linker and SDK/sysroot, but ordinary LLVM-tier builds compile no C. AddressSanitizer, explicit C builds, and LLVM refusals keep the external C-toolchain path.
|
||||
3. **LLVM backend** — `--emit=llvm` writes textual LLVM IR and stops. On supported macOS, Linux, and Windows hosts and for WASI, `--emit=asm|obj` sends LLVM IR to the matching LLVM 22 helper. Executable and library builds use the same LLVM backend.
|
||||
4. **Link** — runtime packs contain precompiled C runtime objects plus the required vendor archives. A hashed manifest selects objects from the program's runtime features, optimization mode, and library state model. The target platform linker and SDK/sysroot produce the executable. Runtime development with AddressSanitizer uses an instrumented C runtime.
|
||||
|
||||
Program objects define <code>main</code> and leave their selected <code>scr_*</code> runtime functions undefined. The <code>scr_runtime_abi_v4</code> reference is a strong link-time compatibility check. <code>--print=native-link-info</code> exposes the exact source runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable.
|
||||
Program objects define <code>main</code> and leave their selected <code>scr_*</code> runtime functions undefined. The <code>scr_runtime_abi_v4</code> reference is a strong link-time compatibility check. <code>--print=native-link-info</code> exposes the exact precompiled runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable.
|
||||
|
||||
Inspect any stage yourself:
|
||||
|
||||
@@ -22,24 +21,17 @@ Inspect any stage yourself:
|
||||
$ scriptc build fib.ts --emit=ir
|
||||
$ ls .scriptc/
|
||||
fib.ir.json
|
||||
$ scriptc build fib.ts --emit=c
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
$ scriptc build fib.ts --emit=llvm
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
$ scriptc build fib.ts --emit=asm
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
fib.s
|
||||
$ scriptc build fib.ts --emit=obj
|
||||
$ ls .scriptc/
|
||||
fib.c
|
||||
fib.ir.json
|
||||
fib.ll
|
||||
fib.o
|
||||
@@ -58,10 +50,10 @@ fib.s
|
||||
|
||||
scriptc's correctness claim is not "we implemented the spec" — it is "we ran your semantics against Node's and they matched." Two lanes enforce it on every change:
|
||||
|
||||
- **The differential corpus** — every corpus program runs under Node *and* as a compiled binary; stdout, stderr, and exit codes must match **byte-for-byte**. Servers are exercised by live client drivers against both implementations. Number formatting was additionally fuzz-verified against Node's `String(x)` on a million random doubles.
|
||||
- **The differential corpus** — every corpus program runs under Node *and* as a compiled binary; stdout, stderr, and exit codes must match exactly. Servers are exercised by live client drivers against both implementations. Number formatting was additionally fuzz-verified against Node's `String(x)` on a million random doubles.
|
||||
- **The memory-safety lane** — the entire corpus re-runs under AddressSanitizer with a reference-count audit at exit; a leak or use-after-free anywhere is a build failure. The same lane is available for your programs as `scriptc build --sanitize`.
|
||||
|
||||
Where matching Node byte-for-byte is impossible or deliberately not the goal (timing internals, error-object internals, aliasing at the dynamic boundary), the divergence is **documented and numbered** — see [Limitations](/limitations). Empty divergence space is load-bearing: the verified-identical list records what has been verified, not merely assumed.
|
||||
Where matching Node exactly is impossible or deliberately not the goal (timing internals, error-object internals, aliasing at the dynamic boundary), the divergence is **documented and numbered** — see [Limitations](/limitations). Empty divergence space is load-bearing: the verified-identical list records what has been verified, not merely assumed.
|
||||
|
||||
## The dynamic island
|
||||
|
||||
@@ -79,7 +71,7 @@ Where matching Node byte-for-byte is impossible or deliberately not the goal (ti
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>packages/compiler</code></td>
|
||||
<td>The frontend (tsc API → IR), the typed IR with validator and serializer, the C and LLVM backends, native-helper integration, and the coverage analyzer.</td>
|
||||
<td>The frontend (tsc API → IR), the typed IR with validator and serializer, the LLVM backend, native-helper integration, and the coverage analyzer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>native/llvm-codegen</code></td>
|
||||
|
||||
@@ -4,7 +4,7 @@ Honesty is the product. The static surface is large but not total, and a few beh
|
||||
|
||||
## Native debugging
|
||||
|
||||
Executable builds with `--optimization=dev` support source breakpoints and native stack frames for statically compiled TypeScript and JavaScript. LLVM builds also describe source locals, parameters, captured bindings, and module globals using their source names and lexical scopes. Numbers and booleans display directly; strings expose their native `ScrStr` layout, including UTF-8 `data` and byte `len`, and tagged unions expose their `tag` and `slot` arms. Other heap values and forward-captured scalar bindings can appear as opaque native pointers. The C backend exposes generated C names and representations. TypeScript expression evaluation, JavaScript object formatting, and stepping through code executed by the dynamic engine are not implemented. Release builds and builds with `--strip` omit these source mappings. macOS uses an adjacent `.dSYM` bundle generated with the Command Line Tools' `dsymutil`.
|
||||
Executable builds with `--optimization=dev` support source breakpoints and native stack frames for statically compiled TypeScript and JavaScript. LLVM builds also describe source locals, parameters, captured bindings, and module globals using their source names and lexical scopes. Numbers and booleans display directly; strings expose their native `ScrStr` layout, including UTF-8 `data` and byte `len`, and tagged unions expose their `tag` and `slot` arms. Other heap values and forward-captured scalar bindings can appear as opaque native pointers. TypeScript expression evaluation, JavaScript object formatting, and stepping through code executed by the dynamic engine are not implemented. Release builds and builds with `--strip` omit these source mappings. macOS uses an adjacent `.dSYM` bundle generated with the Command Line Tools' `dsymutil`.
|
||||
|
||||
## What doesn't compile (yet)
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
# Native Program Objects
|
||||
|
||||
`scriptc build --emit=obj` produces one relocatable program object without
|
||||
invoking clang, a linker, or an SDK on a supported target. The object defines
|
||||
`main`; it is intended to become the program in an external native link. It
|
||||
is not a host-callable library—use `scriptc build --lib --profile ...` for
|
||||
that interface.
|
||||
`scriptc build --emit=obj` produces one relocatable program object without invoking clang, a linker, or an SDK on a supported target. The object defines `main`; it is intended to become the program in an external native link. It is not a host-callable library—use `scriptc build --lib --profile ...` for that interface.
|
||||
|
||||
## ABI and runtime contract
|
||||
|
||||
The external object ABI is **experimental**. Its `scr_*` function and data
|
||||
surface may change before 1.0, so consumers must use the exact
|
||||
`@scriptc/runtime` version reported by the same compiler installation. This
|
||||
is stricter than semver compatibility.
|
||||
The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility.
|
||||
|
||||
The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v4`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
|
||||
|
||||
## Machine-readable link information
|
||||
|
||||
Add `--print=native-link-info` to emit the object and print a JSON document
|
||||
instead of the ordinary path line:
|
||||
Add `--print=native-link-info` to emit the object and print a JSON document instead of the ordinary path line:
|
||||
|
||||
```console
|
||||
$ scriptc build main.ts --print=native-link-info -o app.o > link-info.json
|
||||
@@ -28,23 +20,15 @@ The `scriptc.native-link-info.v1` document reports:
|
||||
|
||||
- target triple, object format, architecture, minimum OS, and relocation model;
|
||||
- the `main` entry and versioned runtime ABI marker;
|
||||
- the matching installed `@scriptc/runtime` source-pack root and exact source
|
||||
sets, include paths, defines, and compile flags selected by the program;
|
||||
- the matching installed precompiled runtime-pack root, selected objects and archives, sizes, and hashes;
|
||||
- ordered program, FFI, runtime, and vendor inputs; and
|
||||
- required system libraries and frameworks.
|
||||
|
||||
Paths inside each source set are relative to `runtime_pack.root`. FFI library
|
||||
paths are the manifest-resolved absolute inputs. No path points into scriptc's
|
||||
private build cache. This external recipe remains source-based for transparency
|
||||
and custom-toolchain embedding. Ordinary scriptc executables instead consume
|
||||
the installed, hashed `@scriptc/runtime-darwin-arm64` object pack and require
|
||||
only the final macOS SDK and linker.
|
||||
Runtime artifact paths are relative to `runtime_pack.root`. FFI library paths are absolute. The document names installed, verified pack artifacts and their link order. The example consumer verifies and copies those artifacts before linking.
|
||||
|
||||
## C compiler as linker driver
|
||||
|
||||
The repository's `examples/native-object` directory is a runnable example
|
||||
with a TypeScript program, a C FFI function, and a small consumer for the JSON
|
||||
recipe:
|
||||
The repository's `examples/native-object` directory is a runnable example with a TypeScript program, a C FFI function, and a small consumer for the JSON recipe:
|
||||
|
||||
```console
|
||||
$ cd examples/native-object
|
||||
@@ -55,17 +39,11 @@ $ ./app-cc
|
||||
42
|
||||
```
|
||||
|
||||
The script is the macOS external-link example: it compiles each reported
|
||||
source set and gives clang only the link inputs and system libraries from the
|
||||
document. `--emit=obj` itself remains clang-free; this compiler invocation
|
||||
belongs to the external runtime build. Linux objects need the target glibc or
|
||||
musl linker/sysroot; Windows MSVC objects need the Windows SDK/MSVC CRT; WASI
|
||||
objects need a Preview 1 linker/sysroot.
|
||||
The example gives clang the reported objects, archives, and system libraries. Linux objects need the target glibc or musl linker/sysroot; Windows x64 objects use Zig's MinGW sysroot with the supplied runtime pack; WASI objects need a Preview 1 linker/sysroot.
|
||||
|
||||
## Native Apple linker
|
||||
|
||||
The same example can invoke Apple `ld` directly after compiling the reported
|
||||
runtime source sets:
|
||||
The same example can invoke Apple `ld` directly with the precompiled runtime objects:
|
||||
|
||||
```console
|
||||
$ node link.mjs ld link-info.json app-ld
|
||||
@@ -73,12 +51,6 @@ $ ./app-ld
|
||||
42
|
||||
```
|
||||
|
||||
This lane asks `xcrun` for the selected macOS SDK and linker, then supplies
|
||||
the target's minimum OS, every ordered object/archive input, and each reported
|
||||
system library. It demonstrates the code-generation boundary precisely:
|
||||
scriptc owns `app.o`; an external toolchain owns runtime compilation and the
|
||||
platform link.
|
||||
This lane asks `xcrun` for the selected macOS SDK and linker, then supplies the target's minimum OS, every ordered object/archive input, and each reported system library. It demonstrates the code-generation boundary precisely: scriptc owns `app.o`; the external toolchain performs the platform link.
|
||||
|
||||
Outbound FFI declarations retain the same C ABI in clang-compiled LLVM and
|
||||
helper-produced object paths. Scalar widths, string/byte pointer-plus-length
|
||||
pairs, and callback signatures follow the [Native FFI](/ffi) manifest.
|
||||
Outbound FFI declarations retain the same C ABI in clang-compiled LLVM and helper-produced object paths. Scalar widths, string/byte pointer-plus-length pairs, and callback signatures follow the [Native FFI](/ffi) manifest.
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
## Owned LLVM targets
|
||||
|
||||
Source artifacts selected with <code>--emit=ir|c|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-tier 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. Explicit C builds, LLVM fallbacks, and <code>--sanitize</code> still require a C compiler. Object output retains undefined runtime references and is not a library archive.
|
||||
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.
|
||||
|
||||
## Cross-compilation via zig
|
||||
|
||||
scriptc cross-compiles from macOS using zig's bundled clang and sysroots. Install [zig](https://ziglang.org), then select it with two environment variables:
|
||||
The installed host LLVM helper emits cross-target program objects and the matching runtime pack supplies precompiled support objects. Install [zig](https://ziglang.org) for Linux, Windows, and WASI cross-linking, then select the output target:
|
||||
|
||||
<dl>
|
||||
<dt><code>SCRIPTC_CC=zigcc</code></dt>
|
||||
<dd>Use zig's clang as the C compiler.</dd>
|
||||
<dt><code>SCRIPTC_TARGET=<triple></code></dt>
|
||||
<dd>The target triple; GNU/Linux targets include a glibc version.</dd>
|
||||
</dl>
|
||||
@@ -18,7 +16,7 @@ scriptc cross-compiles from macOS using zig's bundled clang and sysroots. Instal
|
||||
### Linux (arm64, x86_64)
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
|
||||
$ file fib-linux
|
||||
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
|
||||
```
|
||||
@@ -28,14 +26,14 @@ The runtime has native Linux backends throughout: the event loop is epoll (kqueu
|
||||
### Windows (x86_64)
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=x86_64-windows-gnu scriptc build fib.ts -o fib.exe
|
||||
$ SCRIPTC_TARGET=x86_64-windows-gnu scriptc build fib.ts -o fib.exe
|
||||
$ file fib.exe
|
||||
fib.exe: PE32+ executable (console) x86-64, for MS Windows
|
||||
```
|
||||
|
||||
The full corpus runs in a differential lane against Windows Node, including `--dynamic` programs and `child_process`. The Windows fixture lanes also exercise real loopback traffic through `net`, `http`, `https`, `tls`, `http2`, `dgram`, and `dns`, plus the native `fetch` implementation (including redirects, streaming bodies, compression, cancellation, and proxy handling).
|
||||
|
||||
Windows executable builds use the console subsystem by default. Set <code>--windows-subsystem=gui</code> for an application that owns its own window so Windows does not open an extra console window. This selects the PE subsystem at link time and works with either backend.
|
||||
Windows executable builds use the console subsystem by default. Set <code>--windows-subsystem=gui</code> for an application that owns its own window so Windows does not open an extra console window. This selects the PE subsystem at link time.
|
||||
|
||||
### iOS and Android (arm64, library mode)
|
||||
|
||||
@@ -43,25 +41,25 @@ Three mobile triples build **library-mode static archives** for an embedding app
|
||||
|
||||
<dl>
|
||||
<dt><code>SCRIPTC_TARGET=aarch64-apple-ios</code></dt>
|
||||
<dd>iOS device archives (Mach-O, arm64). Requires a macOS host with Xcode's iPhoneOS SDK. Every object carries <code>LC_BUILD_VERSION</code> with minimum OS 15.0.</dd>
|
||||
<dd>iOS device archives (Mach-O, arm64). Requires a macOS host. The runtime pack was built with Xcode's iPhoneOS SDK. Every object carries <code>LC_BUILD_VERSION</code> with minimum OS 15.0.</dd>
|
||||
<dt><code>SCRIPTC_TARGET=aarch64-apple-ios-simulator</code></dt>
|
||||
<dd>iOS simulator archives (Mach-O, arm64, simulator platform). Requires a macOS host with Xcode's iPhoneSimulator SDK. Same iOS 15.0 minimum.</dd>
|
||||
<dd>iOS simulator archives (Mach-O, arm64, simulator platform). Requires a macOS host. The runtime pack was built with Xcode's iPhoneSimulator SDK. Same iOS 15.0 minimum.</dd>
|
||||
<dt><code>SCRIPTC_TARGET=aarch64-linux-android</code></dt>
|
||||
<dd>Android archives (ELF, arm64) compiled against the NDK's bionic headers at API level 26 — the embedder's <code>minSdkVersion</code> must be 26 or higher. Requires an Android NDK: <code>ANDROID_NDK_ROOT</code>, or the newest <code>ndk/<version></code> under <code>ANDROID_HOME</code> or the default SDK location.</dd>
|
||||
<dd>Android archives (ELF, arm64) compiled against the NDK's bionic headers at API level 26 — the embedder's <code>minSdkVersion</code> must be 26 or higher. Creating the archive uses the precompiled Android runtime pack; the embedding app uses the NDK for its final link.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-apple-ios scriptc build --lib --profile app.profile.json
|
||||
$ SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-android scriptc build --lib --profile app.profile.json
|
||||
$ SCRIPTC_TARGET=aarch64-apple-ios scriptc build --lib --profile app.profile.json
|
||||
$ SCRIPTC_TARGET=aarch64-linux-android scriptc build --lib --profile app.profile.json
|
||||
```
|
||||
|
||||
The full library-mode feature set applies: profile-declared exports and ABI entry points, host-callback channels (`callbacks` + `abi.callback_register_symbol`), contract sidecars, determinism fences, `abi.localize_runtime` (multi-instance archives — Mach-O localization runs the macOS host linker for both iOS platforms; Android rides the same in-process ELF localization as the Linux cross targets), and `abi.instance_per_thread` (thread-instanced state). The archive's external-symbol contract is unchanged: undefined references only to the target's C/math runtime and system APIs, resolved by Xcode's link against the selected SDK or the NDK clang link at API 26+. Simulator and emulator execution of the archive probes is part of the test matrix; device-architecture archives are build- and link-verified.
|
||||
|
||||
### WebAssembly (WASI Preview 1)
|
||||
|
||||
Set <code>SCRIPTC_CC=zigcc</code> and <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>. <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.
|
||||
|
||||
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 debugging C backend remains available for async-free inspection only through an explicit <code>--backend c</code>; coroutine-dependent programs report <code>SC3001</code>, and WASI never silently falls back to C.
|
||||
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.
|
||||
|
||||
@@ -70,7 +68,7 @@ The remaining executable boundary is host capability, not language coverage. WAS
|
||||
- `--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>.
|
||||
- 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 (the Apple SDK sysroot and Mach-O localization); Android targets build from any host with an NDK.
|
||||
- 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.
|
||||
|
||||
## Summary
|
||||
@@ -97,16 +95,16 @@ The remaining executable boundary is host capability, not language coverage. WAS
|
||||
<tr>
|
||||
<td>Windows x86_64</td>
|
||||
<td>native x64 MSVC helper + runtime pack</td>
|
||||
<td>Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; executable linking needs the Windows SDK/MSVC CRT</td>
|
||||
<td>Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; executable linking uses Zig’s Windows sysroot</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS arm64 (device and simulator)</td>
|
||||
<td><code>SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-apple-ios</code> or <code>aarch64-apple-ios-simulator</code>, macOS host with Xcode</td>
|
||||
<td><code>SCRIPTC_TARGET=aarch64-apple-ios</code> or <code>aarch64-apple-ios-simulator</code>, macOS host with Xcode</td>
|
||||
<td>Library mode only (<code>--lib</code>): static archives for Xcode projects, iOS 15.0 minimum; multi-instance and thread-instanced profiles; simulator-executed test matrix</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Android arm64</td>
|
||||
<td><code>SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-android</code>, any host with an NDK</td>
|
||||
<td><code>SCRIPTC_TARGET=aarch64-linux-android</code>, any supported host with Zig</td>
|
||||
<td>Library mode only (<code>--lib</code>): static archives for Gradle/NDK projects, API level 26 minimum; multi-instance and thread-instanced profiles; emulator-executed test matrix</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# External program object
|
||||
|
||||
This macOS arm64 example links a scriptc program object, a small C FFI
|
||||
implementation, and the exact installed source runtime pack. It uses no
|
||||
scriptc cache path.
|
||||
This macOS arm64 example links a scriptc program object, a small C FFI implementation, and the exact installed precompiled runtime pack. It uses no scriptc cache path.
|
||||
|
||||
```console
|
||||
$ clang -target arm64-apple-macosx14.0.0 -O2 -c native.c -o native.o
|
||||
@@ -15,10 +13,6 @@ $ ./app-ld
|
||||
42
|
||||
```
|
||||
|
||||
`cc` uses the C compiler as a linker driver. `ld` compiles the same reported
|
||||
runtime sources and invokes the Apple linker directly with the selected SDK.
|
||||
The object defines `main`; it is a complete program object, not a library to
|
||||
load into another process. Use `scriptc build --lib --profile ...` for a
|
||||
host-callable static library.
|
||||
`cc` uses the C compiler as a linker driver. `ld` links the same precompiled runtime objects directly with the selected Apple SDK. The object defines `main`; it is a complete program object, not a library to load into another process. Use `scriptc build --lib --profile ...` for a host-callable static library.
|
||||
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v3`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v4`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
|
||||
const [mode, infoArg, outputArg] = process.argv.slice(2);
|
||||
if ((mode !== "cc" && mode !== "ld") || !infoArg || !outputArg) {
|
||||
@@ -43,28 +44,17 @@ if ((mode !== "cc" && mode !== "ld") || !infoArg || !outputArg) {
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
const runtimeObjects = [];
|
||||
const vendorArchives = [];
|
||||
for (const set of info.runtime_pack.source_sets) {
|
||||
const setDir = join(buildDir, set.name);
|
||||
mkdirSync(setDir, { recursive: true });
|
||||
const objects = [];
|
||||
for (const source of set.sources) {
|
||||
const object = join(setDir, `${source.replace(/[^A-Za-z0-9]+/g, "_")}.o`);
|
||||
run("clang", [
|
||||
...set.c_flags,
|
||||
...set.defines.map((define) => `-D${define}`),
|
||||
...set.include_directories.flatMap((path) => [
|
||||
"-I", join(info.runtime_pack.root, path),
|
||||
]),
|
||||
"-c", join(info.runtime_pack.root, source), "-o", object,
|
||||
]);
|
||||
objects.push(object);
|
||||
}
|
||||
if (set.output === "objects") {
|
||||
runtimeObjects.push(...objects);
|
||||
} else {
|
||||
const archive = join(buildDir, basename(set.suggested_output));
|
||||
run("ar", ["rcs", archive, ...objects]);
|
||||
vendorArchives.push(archive);
|
||||
for (const [kind, artifacts] of [["objects", info.runtime_pack.objects], ["archives", info.runtime_pack.archives]]) {
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.path.startsWith("/") || artifact.path.split(/[\\/]/).includes("..")) throw new Error("invalid runtime artifact path");
|
||||
const bytes = readFileSync(join(info.runtime_pack.root, artifact.path));
|
||||
if (bytes.length !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) {
|
||||
throw new Error(`runtime pack hash mismatch: ${artifact.path}`);
|
||||
}
|
||||
const destination = join(buildDir, artifact.path);
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
writeFileSync(destination, bytes, { mode: 0o400 });
|
||||
(kind === "objects" ? runtimeObjects : vendorArchives).push(destination);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ set(SCRIPTC_DEFAULT_TARGET "arm64-apple-macosx14.0.0" CACHE STRING
|
||||
set(SCRIPTC_DEFAULT_DATA_LAYOUT
|
||||
"e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"
|
||||
CACHE STRING "Data layout for SCRIPTC_DEFAULT_TARGET")
|
||||
set(SCRIPTC_TARGET_BACKENDS "AArch64" CACHE STRING
|
||||
set(SCRIPTC_TARGET_BACKENDS "AArch64;X86;WebAssembly" CACHE STRING
|
||||
"Semicolon-separated LLVM target backends compiled into the helper")
|
||||
set(SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0" CACHE STRING
|
||||
set(SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0,x86_64-apple-macosx14.0.0,x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,x86_64-pc-windows-msvc,wasm32-unknown-wasi,arm64-apple-ios15.0.0,arm64-apple-ios15.0.0-simulator,aarch64-unknown-linux-android26" CACHE STRING
|
||||
"Comma-separated target triples accepted by this helper")
|
||||
set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING
|
||||
"macOS architecture of the helper executable")
|
||||
|
||||
@@ -118,6 +118,9 @@ std::unique_ptr<TargetMachine> createTargetMachine(StringRef TripleName,
|
||||
if (Definition == nullptr)
|
||||
return nullptr;
|
||||
TargetOptions Options;
|
||||
// The Android API 26 runtime pack uses the NDK's emulated TLS ABI.
|
||||
// Program globals and runtime thread instances must use the same ABI.
|
||||
Options.EmulatedTLS = TargetTriple.isAndroid();
|
||||
return std::unique_ptr<TargetMachine>(Definition->createTargetMachine(
|
||||
TargetTriple, "generic", "", Options, Reloc::PIC_, CodeModel::Small,
|
||||
codeGenLevel(OptLevel)));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Every host helper provides the same set of program object ABIs. */
|
||||
export const nativeTargetCmakeArgs = [
|
||||
"-DSCRIPTC_TARGET_BACKENDS=AArch64;X86;WebAssembly",
|
||||
"-DSCRIPTC_ALLOWED_TARGETS=arm64-apple-macosx14.0.0,x86_64-apple-macosx14.0.0,x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,x86_64-pc-windows-msvc,wasm32-unknown-wasi,arm64-apple-ios15.0.0,arm64-apple-ios15.0.0-simulator,aarch64-unknown-linux-android26",
|
||||
];
|
||||
@@ -22,9 +22,9 @@ $ 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-tier executables use the matching optional helper and precompiled runtime pack, so the driver only links; select that driver with `SCRIPTC_LINKER`. Explicit C builds, LLVM fallbacks, `--sanitize`, and the deprecated `SCRIPTC_CC=clang|zigcc` compatibility route still compile C. `--emit=ir|c|llvm` requires only Node, while `--emit=asm|obj` requires neither an external compiler nor a linker.
|
||||
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.
|
||||
|
||||
Builds use a bounded persistent cache by default. Exact unchanged library builds validate their recorded TypeScript/module-resolution inputs and restore the generated C/LLVM unit before starting the frontend. TypeScript comment-only edits can restore validated lowered IR instead, rebasing source locations and regenerating exact-source build identity before emission; directives, JSDoc-bearing JavaScript, token edits, configuration, package resolution, and newly appearing candidates still invalidate it. Library identity getters live in a tiny C translation unit, so build-id-only changes reuse the large compiled program object and compile only that small member before rearchiving. The native cache then applies its independent toolchain checks. Unchanged executables and library archives skip native code generation and linking after fresh compiler metadata probes, while edited builds reuse stable runtime objects. Experimental provenance-source builds bypass the early frontend tier because their fetched-source registry is process state. FFI builds with archive/object inputs or ambient `system_libraries` relink every time but still reuse runtime objects. Mutable compiler input paths such as `CPATH` and `SDKROOT`, and compiler wrappers, bypass persistent native artifacts and objects so same-path dependency edits cannot go stale. Opaque archiver wrappers rebuild library program members and archives while retaining runtime-object reuse. Direct Clang, Apple's system Clang shim, `zig cc`, trusted platform archivers, and `zig ar` retain their applicable persistent tiers. Set `SCRIPTC_NO_CACHE=1` to bypass every cache or `SCRIPTC_CACHE_DIR` to choose its location; an existing POSIX override must already be private, otherwise caching is bypassed without changing its permissions.
|
||||
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.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -32,17 +32,13 @@ Builds use a bounded persistent cache by default. Exact unchanged library builds
|
||||
- `scriptc run <file.ts>` — compile and run
|
||||
- `scriptc coverage <file.ts>` — what compiles statically, and why the rest doesn't
|
||||
|
||||
`scriptc build app.ts --emit=ir|c|llvm|asm|obj` selects serialized typed IR, readable C, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v4` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
|
||||
`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v4` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
|
||||
|
||||
For embedder-hosted modules that are not installed npm packages, coverage can
|
||||
map an exact bare specifier to a local declaration with repeatable
|
||||
`--external-types <specifier=file.d.ts>` options. This is analysis-only: the
|
||||
types unblock application measurement, while runtime module uses remain
|
||||
reported as blockers.
|
||||
For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types <specifier=file.d.ts>` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers.
|
||||
|
||||
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_CC=zigcc 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, native FFI, and library-mode archive builds are rejected with `SC3002`.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json",
|
||||
"postinstall": "node scripts/warm-cache.mjs"
|
||||
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/compiler": "workspace:*"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export function hostSupportsRuntimePack(
|
||||
platform?: NodeJS.Platform,
|
||||
architecture?: string,
|
||||
hostRelease?: string,
|
||||
): boolean;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { release } from "node:os";
|
||||
|
||||
export function hostSupportsRuntimePack(
|
||||
platform = process.platform,
|
||||
architecture = process.arch,
|
||||
hostRelease = release(),
|
||||
) {
|
||||
const darwinMajor = Number.parseInt(hostRelease.split(".", 1)[0] ?? "", 10);
|
||||
return (platform === "darwin" && (architecture === "arm64" || architecture === "x64") && darwinMajor >= 24) ||
|
||||
(platform === "linux" && (architecture === "arm64" || architecture === "x64")) ||
|
||||
(platform === "win32" && architecture === "x64");
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { hostSupportsRuntimePack } from "./runtime-pack-host.mjs";
|
||||
|
||||
// Workspace installs should stay cheap and deterministic: repository test
|
||||
// images already manage cache warming explicitly. Published npm packages do
|
||||
// not contain src/, so only installed consumers take this best-effort path.
|
||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
if (
|
||||
!existsSync(join(packageRoot, "src")) &&
|
||||
process.env["SCRIPTC_NO_CACHE"] !== "1" &&
|
||||
// A supported platform install already carries immutable release/dev
|
||||
// runtime artifacts. Unsupported hosts retain the source-toolchain path.
|
||||
!hostSupportsRuntimePack()
|
||||
) {
|
||||
try {
|
||||
const { warmNativeCaches } = await import("@scriptc/compiler");
|
||||
await warmNativeCaches();
|
||||
} catch (error) {
|
||||
// Installation must never depend on a native toolchain. A machine without
|
||||
// clang/ar (or with lifecycle scripts disabled) retains ordinary lazy
|
||||
// first-build behavior; explicit `scriptc cache warm` reports errors.
|
||||
if (process.env["SCRIPTC_CACHE_WARM_DEBUG"] === "1") {
|
||||
process.stderr.write(`scriptc: cache warm skipped: ${error instanceof Error ? error.message : String(error)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { enableCompileCache } from "node:module";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
||||
|
||||
// Node 24 can persist V8's compiled module bytecode. scriptc's CLI imports
|
||||
@@ -58,11 +57,11 @@ async function tryFastPath(): Promise<number | null> {
|
||||
(values.emit !== undefined && values.emit !== "exe") ||
|
||||
values.print !== undefined ||
|
||||
values["emit-ir"] ||
|
||||
values.lib || values["from-c"] || values["provenance-sources"] ||
|
||||
values.lib || values["provenance-sources"] ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
) return null;
|
||||
const backend = values.backend;
|
||||
if (backend !== undefined && backend !== "c" && backend !== "llvm") return null;
|
||||
if (backend !== undefined && backend !== "llvm") return null;
|
||||
const optimization = values.optimization;
|
||||
if (optimization !== undefined && optimization !== "release" && optimization !== "dev") return null;
|
||||
const npmRaw = (values["npm-static"] ?? [])
|
||||
@@ -103,7 +102,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
// full compiler. Otherwise a valid helper/runtime-pack cache entry has a
|
||||
// different target/compiler identity and bootstrap must unnecessarily load
|
||||
// the whole compiler graph to rediscover it.
|
||||
const helperRuntimePackTarget = backend !== "c" && !values.sanitize
|
||||
const helperRuntimePackTarget = !values.sanitize
|
||||
? startup.precompiledRuntimePackTarget()
|
||||
: null;
|
||||
const helperObjectRoute = helperRuntimePackTarget !== null;
|
||||
@@ -126,7 +125,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
emitIr: values["emit-ir"],
|
||||
sanitize: values.sanitize,
|
||||
dynamic: values.dynamic,
|
||||
backend: backend ?? "auto",
|
||||
backend: "llvm",
|
||||
...(optimization === "dev" ? { optimization: "dev" as const } : {}),
|
||||
...(values.strip ? { strip: true as const } : {}),
|
||||
npmStatic,
|
||||
@@ -141,18 +140,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
nodeVersion: process.version,
|
||||
});
|
||||
if (hit === null) return null;
|
||||
if (shouldWarnLegacyCExecutable({
|
||||
executable: true,
|
||||
fromC: false,
|
||||
backend,
|
||||
sanitize: values.sanitize,
|
||||
})) {
|
||||
process.stderr.write(LEGACY_C_EXECUTABLE_WARNING);
|
||||
}
|
||||
if (hit.native.llvmRefusal !== undefined) {
|
||||
process.stderr.write(`scriptc: backend c (llvm refused: ${hit.native.llvmRefusal})\n`);
|
||||
}
|
||||
if (!values["keep-c"]) await rm(hit.cPath, { force: true });
|
||||
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
|
||||
if (command === "build") {
|
||||
process.stdout.write(`${outPath}\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
|
||||
const executable = {
|
||||
executable: true,
|
||||
fromC: false,
|
||||
backend: undefined,
|
||||
sanitize: false,
|
||||
};
|
||||
|
||||
test("warns only for generated legacy C executables on runtime-pack hosts", () => {
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang" }, true)).toBe(true);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, fromC: true }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, backend: "c" }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, sanitize: true }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, executable: false }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang", SCRIPTC_TARGET: "wasm32-wasi" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang" }, false)).toBe(false);
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs";
|
||||
|
||||
export const LEGACY_C_EXECUTABLE_WARNING =
|
||||
"scriptc: warning: SCRIPTC_CC selects the deprecated legacy C executable path on macOS arm64; " +
|
||||
"unset it to use the bundled LLVM helper/runtime pack, or use SCRIPTC_LINKER to select the platform linker driver\n";
|
||||
|
||||
export function shouldWarnLegacyCExecutable(options: {
|
||||
executable: boolean;
|
||||
fromC: boolean;
|
||||
backend: "c" | "llvm" | undefined;
|
||||
sanitize: boolean;
|
||||
}, env: NodeJS.ProcessEnv = process.env, runtimePackHost = hostSupportsRuntimePack()): boolean {
|
||||
return options.executable && !options.fromC &&
|
||||
options.backend !== "c" && !options.sanitize &&
|
||||
(env["SCRIPTC_TARGET"] ?? "") === "" &&
|
||||
runtimePackHost &&
|
||||
env["SCRIPTC_CC"] !== undefined && env["SCRIPTC_CC"] !== "";
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { analyze, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, sourceTargetPlatform, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler";
|
||||
import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
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";
|
||||
@@ -71,7 +70,7 @@ async function main(): Promise<number> {
|
||||
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["from-c"] || 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-c"]) {
|
||||
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;
|
||||
@@ -140,7 +139,7 @@ async function main(): Promise<number> {
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
return 1;
|
||||
}
|
||||
if (!values["keep-c"]) rmSync(result.cPath, { force: true });
|
||||
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.
|
||||
@@ -197,8 +196,8 @@ async function main(): Promise<number> {
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "c" && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: c, llvm)\n\n${USAGE}`);
|
||||
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") {
|
||||
@@ -219,8 +218,7 @@ async function main(): Promise<number> {
|
||||
: { emit: values.emit ?? "obj" }),
|
||||
emitIr: values["emit-ir"],
|
||||
...(values.backend === undefined ? {} : { backend: values.backend }),
|
||||
fromC: values["from-c"],
|
||||
keepC: values["keep-c"],
|
||||
keepLlvm: values["keep-llvm"],
|
||||
sanitize: values.sanitize,
|
||||
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
|
||||
strip: values.strip,
|
||||
@@ -275,35 +273,8 @@ async function main(): Promise<number> {
|
||||
}
|
||||
const { outDir, outPath } = selectOutputPaths(input, output.cliOutputKind, values.out);
|
||||
|
||||
// SCRIPTC_CC remains a migration escape hatch for explicit C, sanitizer,
|
||||
// and comparison builds. The normal LLVM executable route is controlled by
|
||||
// SCRIPTC_LINKER, which receives objects and archives only.
|
||||
if (shouldWarnLegacyCExecutable({
|
||||
executable: output.outputKind === "exe",
|
||||
fromC: values["from-c"],
|
||||
backend: values.backend,
|
||||
sanitize: values.sanitize,
|
||||
})) {
|
||||
process.stderr.write(LEGACY_C_EXECUTABLE_WARNING);
|
||||
}
|
||||
|
||||
let nativeLinkInfo: object | undefined;
|
||||
const build = async (): Promise<string> => {
|
||||
if (values["from-c"]) {
|
||||
if (ffiProfilePath !== undefined) {
|
||||
fail("--ffi is a TypeScript/JavaScript compiler feature and cannot be combined with --from-c");
|
||||
}
|
||||
await compileExternalC({
|
||||
cPath: input,
|
||||
outPath,
|
||||
sanitize: values.sanitize,
|
||||
dynamic: values.dynamic,
|
||||
...(optimization !== undefined ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true } : {}),
|
||||
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
|
||||
});
|
||||
return outPath;
|
||||
}
|
||||
const result = await compile(input, {
|
||||
outPath,
|
||||
outDir,
|
||||
@@ -326,16 +297,8 @@ async function main(): Promise<number> {
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
// The lane-change note: the ONLY case where silence would be dishonest
|
||||
// is the default lane quietly building through C — one stderr line
|
||||
// names the refusal. A successful LLVM build is the documented default
|
||||
// (and the kept .ll next to the binary is the durable record), and an
|
||||
// explicit --backend was the user's own choice — neither gets a line.
|
||||
if (result.artifact.kind === "exe") {
|
||||
if (result.artifact.llvmRefusal !== undefined) {
|
||||
process.stderr.write(`scriptc: backend c (llvm refused: ${result.artifact.llvmRefusal})\n`);
|
||||
}
|
||||
if (!values["keep-c"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
} else if (result.artifact.kind === "obj") {
|
||||
nativeLinkInfo = result.artifact.nativeLinkInfo;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ export interface OutputOptionValues {
|
||||
emit?: string;
|
||||
emitIr: boolean;
|
||||
backend?: string;
|
||||
fromC: boolean;
|
||||
keepC: boolean;
|
||||
keepLlvm: boolean;
|
||||
sanitize: boolean;
|
||||
optimization?: string;
|
||||
strip?: boolean;
|
||||
@@ -19,13 +18,13 @@ export type OutputOptionResolution =
|
||||
ok: true;
|
||||
outputKind: CompileOutputKind;
|
||||
cliOutputKind: CliOutputKind;
|
||||
backend?: "c" | "llvm";
|
||||
backend?: "llvm";
|
||||
emitIr: boolean;
|
||||
deprecateEmitIr: boolean;
|
||||
}
|
||||
| { ok: false; message: string };
|
||||
|
||||
const SOURCE_KINDS = new Set<CliOutputKind>(["ir", "c", "llvm"]);
|
||||
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. */
|
||||
@@ -33,18 +32,18 @@ export function resolveOutputOptions(
|
||||
command: "build" | "run",
|
||||
values: OutputOptionValues,
|
||||
): OutputOptionResolution {
|
||||
if (values.backend !== undefined && values.backend !== "c" && values.backend !== "llvm") {
|
||||
return { ok: false, message: `unknown backend "${values.backend}" (supported: c, llvm)` };
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
return { ok: false, message: `unknown backend "${values.backend}" (supported: llvm)` };
|
||||
}
|
||||
const backend = values.backend as "c" | "llvm" | undefined;
|
||||
const backend = values.backend as "llvm" | undefined;
|
||||
const rawEmit = values.emit;
|
||||
if (
|
||||
rawEmit !== undefined && rawEmit !== "ir" && rawEmit !== "c" && rawEmit !== "llvm" &&
|
||||
rawEmit !== undefined && rawEmit !== "ir" && rawEmit !== "llvm" &&
|
||||
rawEmit !== "asm" && rawEmit !== "obj" && rawEmit !== "exe"
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `unknown emit kind "${rawEmit}" (supported: ir, c, llvm, asm, obj, exe)`,
|
||||
message: `unknown emit kind "${rawEmit}" (supported: ir, llvm, asm, obj, exe)`,
|
||||
};
|
||||
}
|
||||
const emit = (rawEmit ?? "exe") as CliOutputKind;
|
||||
@@ -57,9 +56,6 @@ export function resolveOutputOptions(
|
||||
if (values.emitIr && emit === "ir") {
|
||||
return { ok: false, message: `--emit-ir and --emit=ir select the same output; use --emit=ir` };
|
||||
}
|
||||
if (values.fromC && emit !== "exe") {
|
||||
return { ok: false, message: `--from-c only supports --emit=exe` };
|
||||
}
|
||||
if (values.windowsSubsystem !== undefined && emit !== "exe") {
|
||||
return { ok: false, message: `--windows-subsystem is only supported with --emit=exe` };
|
||||
}
|
||||
@@ -69,15 +65,9 @@ export function resolveOutputOptions(
|
||||
if (emit === "ir" && backend !== undefined) {
|
||||
return { ok: false, message: `--emit=ir cannot be combined with --backend; IR is emitted before backend selection` };
|
||||
}
|
||||
if (emit === "c" && backend === "llvm") {
|
||||
return { ok: false, message: `--emit=c cannot be combined with --backend=llvm` };
|
||||
}
|
||||
if ((emit === "llvm" || NATIVE_ARTIFACT_KINDS.has(emit)) && backend === "c") {
|
||||
return { ok: false, message: `--emit=${emit} cannot be combined with --backend=c` };
|
||||
}
|
||||
if (SOURCE_KINDS.has(emit)) {
|
||||
if (!values.keepC) {
|
||||
return { ok: false, message: `--no-keep-c is only meaningful with --emit=exe` };
|
||||
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` };
|
||||
@@ -86,19 +76,15 @@ export function resolveOutputOptions(
|
||||
return { ok: false, message: `--optimization is only meaningful with --emit=exe` };
|
||||
}
|
||||
}
|
||||
if (NATIVE_ARTIFACT_KINDS.has(emit) && !values.keepC) {
|
||||
return { ok: false, message: `--no-keep-c 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 === "c"
|
||||
? { backend: "c" as const }
|
||||
: emit === "llvm" || NATIVE_ARTIFACT_KINDS.has(emit)
|
||||
? { backend: "llvm" as const }
|
||||
: backend === undefined ? {} : { backend }),
|
||||
...(emit === "ir" && backend === undefined ? {} : { backend: "llvm" as const }),
|
||||
emitIr: values.emitIr && emit === "exe",
|
||||
deprecateEmitIr: values.emitIr,
|
||||
};
|
||||
|
||||
@@ -2,11 +2,10 @@ import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { buildTargetPlatform, sourceTargetPlatform, wasiGuestPath } from "@scriptc/compiler";
|
||||
|
||||
export type CliOutputKind = "ir" | "c" | "llvm" | "asm" | "obj" | "exe";
|
||||
export type CliOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
const POSIX_SUFFIXES: Record<CliOutputKind, string> = {
|
||||
ir: ".ir.json",
|
||||
c: ".c",
|
||||
llvm: ".ll",
|
||||
asm: ".s",
|
||||
obj: ".o",
|
||||
|
||||
@@ -18,19 +18,11 @@ Usage:
|
||||
|
||||
Options:
|
||||
-o, --out <path> primary output path (default: .scriptc/<name><suffix>)
|
||||
--emit <kind> primary output: ir, c, llvm, asm, obj, or exe
|
||||
--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 is the default and the output that
|
||||
ships; c emits readable C for inspecting what the
|
||||
compiler produced, and program behavior is identical
|
||||
either way. On native targets, a program outside the LLVM tier still
|
||||
builds — the default lane emits C for it and a one-line
|
||||
stderr note names the construct — while an explicit
|
||||
--backend llvm fails with that construct named
|
||||
wasm32-wasi is LLVM-only unless --backend c is explicit;
|
||||
its C inspection lane accepts async-free programs only
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
uses -O0, source breakpoints, and cached LLVM object shards;
|
||||
@@ -40,11 +32,8 @@ Options:
|
||||
--windows-subsystem <console|gui>
|
||||
Windows executable subsystem (default: console). gui
|
||||
prevents Windows from opening a console window
|
||||
--from-c treat input as a C (or .ll) file (toolchain plumbing/debugging)
|
||||
--keep-c keep the generated program TU next to the executable
|
||||
(default; the .ll — or the .c under --backend=c or
|
||||
when the build fell back)
|
||||
--no-keep-c delete the generated program TU after compiling
|
||||
--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
|
||||
@@ -81,8 +70,7 @@ export const CLI_OPTIONS = {
|
||||
optimization: { type: "string" },
|
||||
strip: { type: "boolean", default: false },
|
||||
"windows-subsystem": { type: "string" },
|
||||
"from-c": { type: "boolean", default: false },
|
||||
"keep-c": { type: "boolean", default: true },
|
||||
"keep-llvm": { type: "boolean", default: true },
|
||||
"emit-ir": { type: "boolean", default: false },
|
||||
sanitize: { type: "boolean", default: false },
|
||||
dynamic: { type: "boolean", default: false },
|
||||
|
||||
@@ -109,9 +109,9 @@ test("bootstrap exact builds use the routed cache and source edits fall through"
|
||||
}, 120_000);
|
||||
|
||||
test.skipIf(!runtimePackHost)(
|
||||
"bootstrap cache hits retain the legacy C executable warning",
|
||||
"bootstrap cache hits reuse LLVM executables",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-legacy-warning-"));
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-llvm-cache-"));
|
||||
const cacheRoot = join(dir, "cache");
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "program");
|
||||
@@ -128,13 +128,13 @@ test.skipIf(!runtimePackHost)(
|
||||
{ env, maxBuffer: 4 * 1024 * 1024 },
|
||||
);
|
||||
try {
|
||||
await writeFile(entry, 'console.log("legacy warning");\n');
|
||||
await writeFile(entry, 'console.log("LLVM cache");\n');
|
||||
const first = await build();
|
||||
expect(first.stderr).toContain("deprecated legacy C executable path");
|
||||
expect((await execFileAsync(outPath)).stdout).toBe("LLVM cache\n");
|
||||
expect(first.stderr).toContain("scriptc lowering");
|
||||
|
||||
const cached = await build();
|
||||
expect(cached.stderr).toContain("deprecated legacy C executable path");
|
||||
expect((await execFileAsync(outPath)).stdout).toBe("LLVM cache\n");
|
||||
expect(cached.stderr).not.toContain("scriptc lowering");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -14,7 +14,7 @@ test("cache warm accepts focused profiles and rejects unknown ones", async () =>
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-cache-warm-"));
|
||||
const cacheRoot = join(dir, "cache");
|
||||
try {
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot };
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_CC: "/unavailable/compiler" };
|
||||
const warmed = await execFileAsync(
|
||||
process.execPath,
|
||||
[bootstrap, "cache", "warm", "runtime"],
|
||||
@@ -23,7 +23,7 @@ test("cache warm accepts focused profiles and rejects unknown ones", async () =>
|
||||
expect(warmed.stdout).toContain(`${cacheRoot}\n`);
|
||||
expect(warmed.stdout).toMatch(/runtime\t\d+ms/);
|
||||
expect(warmed.stdout).not.toContain("tls\t");
|
||||
expect((await readdir(join(cacheRoot, "obj"))).length).toBeGreaterThan(0);
|
||||
expect(await readdir(join(cacheRoot, "obj")).catch(() => [])).toEqual([]);
|
||||
|
||||
await expect(
|
||||
execFileAsync(process.execPath, [bootstrap, "cache", "warm", "unknown"], { env }),
|
||||
@@ -33,9 +33,7 @@ test("cache warm accepts focused profiles and rejects unknown ones", async () =>
|
||||
execFileAsync(process.execPath, [bootstrap, "cache", "warm", "runtime"], {
|
||||
env: { ...env, CPATH: dir },
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
stderr: expect.stringContaining("requires a persistently cacheable compiler environment"),
|
||||
});
|
||||
).resolves.toMatchObject({ stdout: expect.stringContaining("runtime\t") });
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { release as osRelease, tmpdir } from "node:os";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
@@ -13,8 +13,6 @@ const require = createRequire(import.meta.url);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
||||
const runtimePackHost = process.platform === "darwin" && process.arch === "arm64" &&
|
||||
Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) >= 24;
|
||||
const helperTarget = precompiledRuntimePackTarget();
|
||||
const persistentExecutable = helperTarget === null || platformLinkerSupportsPersistentCache(process.env, helperTarget);
|
||||
|
||||
@@ -82,68 +80,12 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta
|
||||
expect(await run()).toBe("two\n");
|
||||
expect((await build()).stderr).not.toContain("scriptc lowering");
|
||||
|
||||
// The explicit backend is a cache-key input; it cannot consume auto's
|
||||
// LLVM result even when source and output paths are identical.
|
||||
expect((await build(["--backend", "c"], {
|
||||
SCRIPTC_LINKER: join(dir, "first-unused-linker"),
|
||||
})).stderr).toContain("scriptc lowering");
|
||||
expect(await readFile(join(dir, "main.c"), "utf8")).toContain("Generated by scriptc");
|
||||
expect(await readFile(tuPath, "utf8")).toContain("two");
|
||||
if (runtimePackHost) {
|
||||
// C builds are keyed by their compiler environment. Changing the
|
||||
// object-only linker must not turn an exact C repeat into a miss.
|
||||
expect((await build(["--backend", "c"], {
|
||||
SCRIPTC_LINKER: join(dir, "second-unused-linker"),
|
||||
})).stderr).not.toContain("scriptc lowering");
|
||||
}
|
||||
|
||||
// Native optimization posture is independently keyed too: dev cannot
|
||||
// consume a release executable or TU, and then exact dev repeats hit.
|
||||
expect((await build(["--optimization", "dev"])).stderr).toContain("scriptc lowering");
|
||||
expect(await run()).toBe("two\n");
|
||||
expect((await build(["--optimization", "dev"])).stderr).not.toContain("scriptc lowering");
|
||||
expect(await readFile(join(dir, "main.c"), "utf8")).toContain("Generated by scriptc");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
test("--from-c forwards the dev optimization posture", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-from-c-optimization-"));
|
||||
const entry = join(dir, "main.c");
|
||||
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
|
||||
try {
|
||||
await writeFile(entry, [
|
||||
"int main(void) {",
|
||||
"#ifdef __OPTIMIZE__",
|
||||
" return 19;",
|
||||
"#else",
|
||||
" return 0;",
|
||||
"#endif",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"));
|
||||
const result = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
tsxLoader,
|
||||
cliEntry,
|
||||
"build",
|
||||
entry,
|
||||
"--from-c",
|
||||
"--optimization",
|
||||
"dev",
|
||||
"-o",
|
||||
outPath,
|
||||
],
|
||||
{
|
||||
env: { ...process.env, SCRIPTC_CC: "clang" },
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
expect(result.stderr).not.toContain("deprecated legacy C executable path");
|
||||
await expect(execFileAsync(outPath)).resolves.toMatchObject({ stdout: "" });
|
||||
expect(await readFile(tuPath, "utf8")).toContain("define i32 @main");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ test("library identity source stays private and cannot overwrite a sidecar", asy
|
||||
profile_format: 1,
|
||||
name: "cli-library-output",
|
||||
entry: "lib.ts",
|
||||
emission: "c",
|
||||
emission: "llvm",
|
||||
abi: {
|
||||
prefix: "clo_",
|
||||
init_symbol: "clo_init",
|
||||
@@ -64,12 +64,12 @@ test("library identity source stays private and cannot overwrite a sidecar", asy
|
||||
},
|
||||
}, null, 2)}\n`,
|
||||
);
|
||||
const runBuild = async (keepC = false, emitIr = false): Promise<{ stderr: string }> => {
|
||||
const runBuild = async (keepLlvm = false, emitIr = false): Promise<{ stderr: string }> => {
|
||||
return execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import", tsxLoader, cliEntry, "build", "--lib", "--profile", profilePath,
|
||||
...(keepC ? [] : ["--no-keep-c"]),
|
||||
...(keepLlvm ? [] : ["--no-keep-llvm"]),
|
||||
...(emitIr ? ["--emit-ir"] : []),
|
||||
],
|
||||
{
|
||||
@@ -83,43 +83,41 @@ test("library identity source stays private and cannot overwrite a sidecar", asy
|
||||
);
|
||||
};
|
||||
|
||||
// --no-keep-c removes the public program TU, and the identity source is
|
||||
// invocation-private rather than a second caller-visible C artifact.
|
||||
// --no-keep-llvm removes the public program TU, and the identity source is
|
||||
// invocation-private rather than a second caller-visible LLVM artifact.
|
||||
await writeProfile("contract.json");
|
||||
await runBuild();
|
||||
expect((await readdir(outDir)).sort()).toEqual(["contract.json", "lib.lib.a"]);
|
||||
|
||||
// A single-source comment-only edit takes the semantic cache path. Its
|
||||
// restored public TU must match a forced miss before --no-keep-c removes
|
||||
// restored public TU must match a forced miss before --no-keep-llvm removes
|
||||
// it again.
|
||||
await writeFile(
|
||||
join(dir, "lib.ts"),
|
||||
`/* harmless rebuild comment */ ${await readFile(join(dir, "lib.ts"), "utf8")}`,
|
||||
);
|
||||
await runBuild(true);
|
||||
const semanticHitC = await readFile(join(outDir, "lib.lib.c"), "utf8");
|
||||
const semanticHitLlvm = await readFile(join(outDir, "lib.lib.ll"), "utf8");
|
||||
await rm(join(cacheRoot, "early-lib"), { recursive: true, force: true });
|
||||
await runBuild(true);
|
||||
expect(await readFile(join(outDir, "lib.lib.c"), "utf8")).toBe(semanticHitC);
|
||||
expect(await readFile(join(outDir, "lib.lib.ll"), "utf8")).toBe(semanticHitLlvm);
|
||||
await runBuild();
|
||||
expect((await readdir(outDir)).sort()).toEqual(["contract.json", "lib.lib.a"]);
|
||||
|
||||
// A line-shifting edit cannot safely reuse line-only annotations (not even
|
||||
// synthetic byte-zero locations). It must match a forced frontend miss.
|
||||
// A line-shifting edit must preserve the same LLVM artifact as a
|
||||
// forced frontend miss.
|
||||
await writeFile(join(dir, "lib.ts"), [
|
||||
"// line-shifting rebuild comment",
|
||||
await readFile(join(dir, "lib.ts"), "utf8"),
|
||||
].join("\n"));
|
||||
await runBuild(true);
|
||||
const shiftedC = await readFile(join(outDir, "lib.lib.c"), "utf8");
|
||||
const shiftedLlvm = await readFile(join(outDir, "lib.lib.ll"), "utf8");
|
||||
await rm(join(cacheRoot, "early-lib"), { recursive: true, force: true });
|
||||
await runBuild(true);
|
||||
expect(await readFile(join(outDir, "lib.lib.c"), "utf8")).toBe(shiftedC);
|
||||
expect(await readFile(join(outDir, "lib.lib.ll"), "utf8")).toBe(shiftedLlvm);
|
||||
|
||||
// Move to a multi-source graph and seed its cache. Imported trivia is
|
||||
// semantically unchanged too, but cached C annotations cannot be rebased
|
||||
// through the entry-only line table. That shape must take the normal
|
||||
// frontend path and match a forced cache miss.
|
||||
// Imported trivia edits in a multi-source graph must also produce
|
||||
// the same LLVM artifact as a forced frontend miss.
|
||||
await writeFile(join(dir, "lib.ts"), (await readFile(join(dir, "lib.ts"), "utf8"))
|
||||
.replace(
|
||||
"export interface Model",
|
||||
@@ -132,14 +130,14 @@ test("library identity source stays private and cannot overwrite a sidecar", asy
|
||||
await readFile(join(dir, "helper.ts"), "utf8"),
|
||||
].join("\n"));
|
||||
await runBuild(true);
|
||||
const fallbackC = await readFile(join(outDir, "lib.lib.c"), "utf8");
|
||||
const savedLlvm = await readFile(join(outDir, "lib.lib.ll"), "utf8");
|
||||
await rm(join(cacheRoot, "early-lib"), { recursive: true, force: true });
|
||||
await runBuild(true);
|
||||
expect(await readFile(join(outDir, "lib.lib.c"), "utf8")).toBe(fallbackC);
|
||||
expect(await readFile(join(outDir, "lib.lib.ll"), "utf8")).toBe(savedLlvm);
|
||||
|
||||
// This name collided with the former fixed `<stem>.lib.identity.c`
|
||||
// output. Repeat to exercise the exact early-cache-hit ordering that used
|
||||
// to restore JSON and then overwrite it with generated C.
|
||||
// to restore JSON and then overwrite it with generated LLVM.
|
||||
await rm(outDir, { recursive: true, force: true });
|
||||
await writeProfile("lib.lib.identity.c");
|
||||
await runBuild();
|
||||
|
||||
@@ -45,7 +45,7 @@ test("print option validation is explicit", async () => {
|
||||
});
|
||||
|
||||
describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
test("prints a stable cache-independent source-pack recipe", async () => {
|
||||
test("prints a stable cache-independent precompiled runtime recipe", async () => {
|
||||
const { entry, object } = await fixture();
|
||||
const { stdout, stderr } = await cli([
|
||||
"build", entry, "--print=native-link-info", "-o", object,
|
||||
@@ -65,19 +65,18 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
|
||||
runtime_pack: {
|
||||
kind: "source",
|
||||
package: "@scriptc/runtime",
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
path_base: "runtime_pack.root",
|
||||
},
|
||||
link: { system_libraries: ["System"], frameworks: [] },
|
||||
});
|
||||
expect(info.runtime_pack.root).not.toContain("node_modules/.cache");
|
||||
expect(info.link.input_order.join("\n")).not.toContain("node_modules/.cache");
|
||||
const runtime = info.runtime_pack.source_sets.find((set) => set.name === "runtime");
|
||||
expect(runtime?.sources).toEqual(expect.arrayContaining([
|
||||
"src/scr_console.c",
|
||||
"src/scr_async.c",
|
||||
"src/scr_cycle.c",
|
||||
expect(info.runtime_pack.objects.map((object) => object.path)).toEqual(expect.arrayContaining([
|
||||
"artifacts/release/runtime/default/scr_console.o",
|
||||
"artifacts/release/runtime/default/scr_async.o",
|
||||
"artifacts/release/runtime/default/scr_cycle.o",
|
||||
]));
|
||||
await expect(readFile(object)).resolves.toBeInstanceOf(Buffer);
|
||||
});
|
||||
@@ -115,14 +114,10 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
"build", entry, "--print=native-link-info", "-o", object,
|
||||
]);
|
||||
const info = JSON.parse(stdout) as NativeLinkInfo;
|
||||
const runtime = info.runtime_pack.source_sets.find((set) => set.name === "runtime")!;
|
||||
const executable = join(dir, "app");
|
||||
await execFileAsync("clang", [
|
||||
...runtime.c_flags,
|
||||
...runtime.defines.map((define) => `-D${define}`),
|
||||
...runtime.include_directories.flatMap((path) => ["-I", join(info.runtime_pack.root, path)]),
|
||||
...runtime.sources.map((path) => join(info.runtime_pack.root, path)),
|
||||
object,
|
||||
...info.link.driver_flags,
|
||||
...info.link.input_order,
|
||||
...info.link.system_libraries.map((name) => `-l${name}`),
|
||||
"-o", executable,
|
||||
]);
|
||||
|
||||
@@ -3,18 +3,15 @@ import { resolveOutputOptions, type OutputOptionValues } from "../src/output-opt
|
||||
|
||||
const BASE: OutputOptionValues = {
|
||||
emitIr: false,
|
||||
fromC: false,
|
||||
keepC: true,
|
||||
keepLlvm: true,
|
||||
sanitize: false,
|
||||
};
|
||||
|
||||
describe("output option compatibility", () => {
|
||||
test.each([
|
||||
[undefined, undefined, "exe", undefined],
|
||||
["exe", "c", "exe", "c"],
|
||||
[undefined, undefined, "exe", "llvm"],
|
||||
["exe", "llvm", "exe", "llvm"],
|
||||
["ir", undefined, "ir", undefined],
|
||||
["c", undefined, "c", "c"],
|
||||
["c", "c", "c", "c"],
|
||||
["llvm", undefined, "llvm", "llvm"],
|
||||
["llvm", "llvm", "llvm", "llvm"],
|
||||
["asm", undefined, "asm", "llvm"],
|
||||
@@ -34,15 +31,14 @@ describe("output option compatibility", () => {
|
||||
|
||||
test.each([
|
||||
[{ emit: "wat" }, /unknown emit kind/],
|
||||
[{ emit: "asm", backend: "c" }, /cannot be combined/],
|
||||
[{ emit: "obj", backend: "c" }, /cannot be combined/],
|
||||
[{ emit: "c", backend: "llvm" }, /cannot be combined/],
|
||||
[{ emit: "llvm", backend: "c" }, /cannot be combined/],
|
||||
[{ emit: "ir", keepC: false }, /no-keep-c/],
|
||||
[{ emit: "c", sanitize: true }, /sanitize/],
|
||||
[{ emit: "asm", backend: "c" }, /unknown backend/],
|
||||
[{ emit: "obj", backend: "c" }, /unknown backend/],
|
||||
[{ emit: "c" }, /unknown emit kind/],
|
||||
[{ emit: "llvm", backend: "c" }, /unknown backend/],
|
||||
[{ emit: "ir", keepLlvm: false }, /no-keep-llvm/],
|
||||
[{ emit: "llvm", sanitize: true }, /sanitize/],
|
||||
[{ emit: "llvm", optimization: "dev" }, /optimization/],
|
||||
[{ emit: "ir", backend: "c" }, /before backend selection/],
|
||||
[{ emit: "ir", fromC: true }, /from-c/],
|
||||
[{ emit: "ir", backend: "llvm" }, /before backend selection/],
|
||||
[{ emit: "llvm", emitIr: true }, /emit-ir/],
|
||||
[{ emit: "ir", emitIr: true }, /same output/],
|
||||
[{ backend: "wat" }, /unknown backend/],
|
||||
@@ -55,7 +51,7 @@ describe("output option compatibility", () => {
|
||||
expect(result).toEqual({ ok: false, message: expect.stringMatching(message) });
|
||||
});
|
||||
|
||||
test.each(["ir", "c", "llvm"])("run rejects --emit=%s", (emit) => {
|
||||
test.each(["ir", "llvm"])("run rejects --emit=%s", (emit) => {
|
||||
expect(resolveOutputOptions("run", { ...BASE, emit })).toEqual({
|
||||
ok: false,
|
||||
message: "scriptc run requires --emit=exe",
|
||||
|
||||
@@ -11,12 +11,12 @@ test("default executable names use the Windows PE suffix", () => {
|
||||
});
|
||||
|
||||
test("source artifact names use stable POSIX and Windows suffixes", () => {
|
||||
expect(["ir", "c", "llvm", "asm", "obj", "exe"].map((kind) =>
|
||||
expect(["ir", "llvm", "asm", "obj", "exe"].map((kind) =>
|
||||
defaultOutputName("main", kind as Parameters<typeof defaultOutputName>[1], "linux")
|
||||
)).toEqual(["main.ir.json", "main.c", "main.ll", "main.s", "main.o", "main"]);
|
||||
expect(["ir", "c", "llvm", "asm", "obj", "exe"].map((kind) =>
|
||||
)).toEqual(["main.ir.json", "main.ll", "main.s", "main.o", "main"]);
|
||||
expect(["ir", "llvm", "asm", "obj", "exe"].map((kind) =>
|
||||
defaultOutputName("main", kind as Parameters<typeof defaultOutputName>[1], "win32")
|
||||
)).toEqual(["main.ir.json", "main.c", "main.ll", "main.asm", "main.obj", "main.exe"]);
|
||||
)).toEqual(["main.ir.json", "main.ll", "main.asm", "main.obj", "main.exe"]);
|
||||
});
|
||||
|
||||
test("primary output selection keeps explicit paths exact", () => {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs";
|
||||
|
||||
describe("runtime-pack host support", () => {
|
||||
test("recognizes each platform with a release-built runtime pack", () => {
|
||||
expect(hostSupportsRuntimePack("darwin", "arm64", "24.0.0")).toBe(true);
|
||||
expect(hostSupportsRuntimePack("darwin", "x64", "24.0.0")).toBe(true);
|
||||
expect(hostSupportsRuntimePack("darwin", "arm64", "23.6.0")).toBe(false);
|
||||
expect(hostSupportsRuntimePack("linux", "x64", "6.8.0")).toBe(true);
|
||||
expect(hostSupportsRuntimePack("linux", "arm64", "6.8.0")).toBe(true);
|
||||
expect(hostSupportsRuntimePack("win32", "x64", "10.0.0")).toBe(true);
|
||||
expect(hostSupportsRuntimePack("linux", "ia32", "6.8.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -65,24 +65,16 @@ describe.runIf(supported)("precompiled runtime executable builds", () => {
|
||||
expect(await readFile(output)).toEqual(firstExecutable);
|
||||
});
|
||||
|
||||
test("the object-plus-runtime-pack route remains live with the legacy C pipeline disabled", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-no-legacy-"));
|
||||
test("library builds use LLVM and precompiled runtime objects even with an unusable C compiler", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-library-pack-cli-"));
|
||||
dirs.push(dir);
|
||||
const entry = join(dir, "main.ts");
|
||||
const output = join(dir, "program");
|
||||
await writeFile(entry, 'console.log("no legacy C");\n');
|
||||
await execFileAsync(
|
||||
process.execPath,
|
||||
["--import", tsxLoader, cliEntry, "build", entry, "-o", output],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
SCRIPTC_NO_CACHE: "1",
|
||||
SCRIPTC_LEGACY_C_PIPELINE: "0",
|
||||
},
|
||||
},
|
||||
);
|
||||
await expect(execFileAsync(output, [], { encoding: "utf8" }))
|
||||
.resolves.toMatchObject({ stdout: "no legacy C\n" });
|
||||
const entry = join(dir, "library.ts"), profile = join(dir, "profile.json"), output = join(dir, "library.a");
|
||||
await writeFile(entry, 'export function answer(): number { return 42; }\n');
|
||||
const template = JSON.parse(await readFile(join(repoRoot, "tests/library-mode/scalars/profile.json"), "utf8"));
|
||||
await writeFile(profile, JSON.stringify({ ...template, entry, emission: "llvm", exports: [{ export: "answer", symbol: "kt_answer", params: [], returns: "f64" }] }));
|
||||
await execFileAsync(process.execPath, ["--import", tsxLoader, cliEntry, "build", "--lib", "--profile", profile, "-o", output], {
|
||||
env: { ...process.env, SCRIPTC_NO_CACHE: "1", SCRIPTC_CC: join(dir, "missing-c-compiler") },
|
||||
});
|
||||
expect((await execFileAsync("ar", ["t", output])).stdout).toContain("scr_library.o");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,9 +35,9 @@ function cli(args: string[], env: NodeJS.ProcessEnv = process.env) {
|
||||
|
||||
test("default and explicit paths identify each source primary artifact", async () => {
|
||||
const { dir, entry } = await fixture();
|
||||
const expected = { ir: "hello.ir.json", c: "hello.c", llvm: "hello.ll" } as const;
|
||||
const expected = { ir: "hello.ir.json", llvm: "hello.ll" } as const;
|
||||
const artifacts: string[] = [];
|
||||
for (const kind of ["ir", "c", "llvm"] as const) {
|
||||
for (const kind of ["ir", "llvm"] as const) {
|
||||
const result = await cli(["build", entry, `--emit=${kind}`]);
|
||||
const path = join(dir, ".scriptc", expected[kind]);
|
||||
artifacts.push(expected[kind]);
|
||||
@@ -110,7 +110,7 @@ test("source outputs never execute compiler, archiver, or linker traps", async (
|
||||
SCRIPTC_TARGET: "aarch64-apple-ios",
|
||||
SCRIPTC_CACHE_DIR: join(dir, "cache-must-not-exist"),
|
||||
};
|
||||
for (const kind of ["ir", "c", "llvm"] as const) {
|
||||
for (const kind of ["ir", "llvm"] as const) {
|
||||
const path = join(dir, `hello.${kind}`);
|
||||
await expect(cli(["build", entry, `--emit=${kind}`, "-o", path], env)).resolves.toMatchObject({
|
||||
stderr: "",
|
||||
@@ -145,7 +145,7 @@ test("--emit-ir remains additive but reports its deprecation", async () => {
|
||||
|
||||
test("run and incompatible compatibility flags reject non-executable outputs", async () => {
|
||||
const { entry } = await fixture();
|
||||
await expect(cli(["run", entry, "--emit=c"])).rejects.toMatchObject({
|
||||
await expect(cli(["run", entry, "--emit=llvm"])).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stderr: expect.stringContaining("scriptc run requires --emit=exe"),
|
||||
});
|
||||
|
||||
@@ -13,14 +13,14 @@ const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
|
||||
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
|
||||
|
||||
test.each(["auto", "c", "from-c"] as const)(
|
||||
test.each(["default", "llvm"] as const)(
|
||||
"--strip shrinks and runs %s executables without mixing cache variants",
|
||||
async (route) => {
|
||||
const dir = await mkdtemp(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-strip-"));
|
||||
const entry = join(dir, route === "from-c" ? "main.c" : "main.ts");
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
|
||||
const routeArgs = route === "auto" ? [] : route === "c" ? ["--backend=c"] : ["--from-c"];
|
||||
const routeArgs = route === "default" ? [] : ["--backend=llvm"];
|
||||
const build = async (strip: boolean, bootstrap = false): Promise<number> => {
|
||||
await execFileAsync(process.execPath, [
|
||||
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
|
||||
@@ -29,14 +29,12 @@ test.each(["auto", "c", "from-c"] as const)(
|
||||
return (await stat(outPath)).size;
|
||||
};
|
||||
try {
|
||||
await writeFile(entry, route === "from-c"
|
||||
? '#include <stdio.h>\nint main(void) { puts("strip-ok"); return 0; }\n'
|
||||
: 'console.log("strip-ok");\n');
|
||||
await writeFile(entry, 'console.log("strip-ok");\n');
|
||||
const originalSize = await build(false);
|
||||
const strippedSize = await build(true);
|
||||
expect(strippedSize).toBeLessThan(originalSize);
|
||||
expect((await execFileAsync(outPath)).stdout).toBe("strip-ok\n");
|
||||
if (route === "auto") {
|
||||
if (route === "default") {
|
||||
expect(await build(true, true)).toBe(strippedSize);
|
||||
expect(await build(false, true)).toBe(originalSize);
|
||||
expect((await execFileAsync(outPath)).stdout).toBe("strip-ok\n");
|
||||
|
||||
@@ -35,7 +35,6 @@ test.runIf(supported)("WASI helper object plus runtime pack builds and runs with
|
||||
...process.env,
|
||||
SCRIPTC_TARGET: "wasm32-wasi",
|
||||
SCRIPTC_NO_CACHE: "1",
|
||||
SCRIPTC_LEGACY_C_PIPELINE: "0",
|
||||
ZIG_GLOBAL_CACHE_DIR: join(zigCache, "global"),
|
||||
ZIG_LOCAL_CACHE_DIR: join(zigCache, "local"),
|
||||
},
|
||||
@@ -59,7 +58,7 @@ test.runIf(supported)("switching WASI LLVM and native C builds preserves both tr
|
||||
await build([], wasiEnv);
|
||||
const llvm = await readFile(join(outDir, "hello.ll"));
|
||||
const wasm = await readFile(join(outDir, "hello.wasm"));
|
||||
await build(["--backend=c", "--keep-c"], nativeEnv);
|
||||
await build(["--backend=llvm", "--keep-llvm"], nativeEnv);
|
||||
const c = await readFile(join(outDir, "hello.c"));
|
||||
expect(await readFile(join(outDir, "hello.ll"))).toEqual(llvm);
|
||||
expect(await readFile(join(outDir, "hello.wasm"))).toEqual(wasm);
|
||||
|
||||
@@ -81,8 +81,7 @@ test.skipIf(process.platform !== "win32" && process.env["SCRIPTC_WIN_SUBSYSTEM"]
|
||||
ZIG_GLOBAL_CACHE_DIR: join(dir, "zig-cache"),
|
||||
};
|
||||
const llvmOut = join(dir, "llvm.exe");
|
||||
const cOut = join(dir, "c.exe");
|
||||
const build = async (backend: "llvm" | "c", out: string, subsystem?: "console" | "gui") => {
|
||||
const build = async (backend: "llvm", out: string, subsystem?: "console" | "gui") => {
|
||||
await cli([
|
||||
"build", entry, `--backend=${backend}`, "-o", out,
|
||||
...(subsystem === undefined ? [] : [`--windows-subsystem=${subsystem}`]),
|
||||
@@ -93,14 +92,8 @@ test.skipIf(process.platform !== "win32" && process.env["SCRIPTC_WIN_SUBSYSTEM"]
|
||||
expect(await build("llvm", llvmOut, "gui")).toBe(2);
|
||||
expect(await build("llvm", llvmOut)).toBe(3);
|
||||
expect(await build("llvm", llvmOut, "console")).toBe(3);
|
||||
expect(await build("c", cOut, "gui")).toBe(2);
|
||||
const cSource = join(dir, "from-c.c");
|
||||
const fromCOut = join(dir, "from-c.exe");
|
||||
await writeFile(cSource, "int main(void) { return 0; }\n");
|
||||
await cli(["build", cSource, "--from-c", "--windows-subsystem=gui", "-o", fromCOut], env);
|
||||
expect(await peSubsystem(fromCOut)).toBe(2);
|
||||
if (process.platform === "win32") {
|
||||
expect((await execFileAsync(cOut)).stdout.trim()).toBe("subsystem");
|
||||
expect((await execFileAsync(llvmOut)).stdout.trim()).toBe("subsystem");
|
||||
}
|
||||
},
|
||||
180_000,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.1.7",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM and C backends",
|
||||
"description": "The scriptc compiler \u2014 TypeScript/JavaScript frontend, typed IR, LLVM backend",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
"repository": {
|
||||
@@ -51,6 +51,9 @@
|
||||
"@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-wasm32-wasi": "workspace:*",
|
||||
"@scriptc/runtime-ios-arm64": "workspace:*",
|
||||
"@scriptc/runtime-ios-simulator-arm64": "workspace:*",
|
||||
"@scriptc/runtime-android-arm64": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
import type { CEmitter } from "./c-emitter.js";
|
||||
import type { IrGlobal, IrLocal } from "../../ir/ir.js";
|
||||
import { mangleGlobal, mangleLocal } from "../mangle.js";
|
||||
import { boxAccess, cType } from "./types.js";
|
||||
|
||||
/** The empty payload slot is the TDZ sentinel for both references and
|
||||
* scalar cells. Check before reading, or after evaluating a write's RHS. */
|
||||
export function checkTdz(emitter: CEmitter, local: IrLocal): void {
|
||||
const box = mangleLocal(local.id);
|
||||
checkEmptyBinding(emitter, `${box}->slot == 0`, local.name);
|
||||
}
|
||||
|
||||
export function checkGlobalTdz(emitter: CEmitter, global: IrGlobal): void {
|
||||
if (global.tdz) checkEmptyBinding(emitter, `${mangleGlobal(global.id)} == NULL`, global.name);
|
||||
}
|
||||
|
||||
function checkEmptyBinding(emitter: CEmitter, condition: string, name: string): void {
|
||||
const errName = emitter.internLiteral("ReferenceError");
|
||||
const message = emitter.internLiteral(`Cannot access '${name}' before initialization`);
|
||||
emitter.line(`if (${condition}) { /* temporal dead zone */`);
|
||||
emitter.indent++;
|
||||
emitter.line(`scr_throw_error_named((ScrStr *)&${errName}, (ScrStr *)&${message});`);
|
||||
emitter.emitUnwind();
|
||||
emitter.indent--;
|
||||
emitter.line("}");
|
||||
}
|
||||
|
||||
/** Read a shared binding. Reference results own a retain; scalar cells
|
||||
* are borrowed from their live box and return a copied value. */
|
||||
export function readBox(emitter: CEmitter, local: IrLocal): string {
|
||||
const box = mangleLocal(local.id);
|
||||
const acc = boxAccess(local.type);
|
||||
if (local.tdz) checkTdz(emitter, local);
|
||||
if (acc === "ref") return `(${cType(local.type).trim()})scr_box_get_ref(${box})`;
|
||||
return local.tdz
|
||||
? `scr_arr_get_${acc}((ScrArr *)(uintptr_t)${box}->slot, 0)`
|
||||
: `scr_box_get_${acc}(${box})`;
|
||||
}
|
||||
|
||||
/** Store an owned payload. A mutable scalar TDZ box keeps the same cell
|
||||
* after initialization, so every closure sees subsequent assignments. */
|
||||
export function writeBox(emitter: CEmitter, local: IrLocal, value: string, initializes = false): void {
|
||||
const box = mangleLocal(local.id);
|
||||
const acc = boxAccess(local.type);
|
||||
if (local.tdz) {
|
||||
// Historical IR represents a const's declaration with plain assign.
|
||||
// Such bindings have no legal subsequent assignment; keep it readable.
|
||||
const first = initializes || !local.mutable;
|
||||
if (!first) checkTdz(emitter, local);
|
||||
if (acc !== "ref") {
|
||||
if (first) {
|
||||
const cell = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`ScrArr *${cell} = ${emitter.arrNewC(local.type, 1)};`);
|
||||
emitter.line(`scr_arr_push_${acc}(${cell}, ${value});`);
|
||||
emitter.line(`scr_box_set_ref(${box}, ${cell});`);
|
||||
} else {
|
||||
emitter.line(`scr_arr_set_${acc}((ScrArr *)(uintptr_t)${box}->slot, 0, ${value});`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
emitter.line(`scr_box_set_${acc}(${box}, ${value});`);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { IR_VERSION } from "../../ir/serialize.js";
|
||||
import { emitCModule, emitCModuleChunks } from "./c-emitter.js";
|
||||
|
||||
test("chunked C emission preserves separators, Unicode and the final newline", () => {
|
||||
const loc = { file: "source-🦀.ts", start: 0, end: 1 };
|
||||
const mod: IrModule = {
|
||||
irVersion: IR_VERSION, sourceFile: loc.file, entry: "main",
|
||||
functions: [{ name: "main", params: [], locals: [], returnType: VOID, loc,
|
||||
body: Array.from({ length: 2000 }, (_, i) => ({ kind: "exprStmt" as const, loc,
|
||||
expr: { kind: "strLit" as const, value: `Unicode 🦀 ${i}`, type: STRING, loc },
|
||||
})),
|
||||
}],
|
||||
};
|
||||
const chunks = emitCModuleChunks(mod);
|
||||
expect(chunks.length).toBeGreaterThan(2);
|
||||
expect(chunks.join("")).toBe(emitCModule(mod));
|
||||
expect(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString()).toBe(emitCModule(mod));
|
||||
expect(chunks.at(-1)!.endsWith("\n")).toBe(true);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,332 +0,0 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Island-boundary C emission owned pieces: the embedded npm module/edge
|
||||
* tables (--dynamic builds embed every reached npm source; the island's
|
||||
* module loader and require shim read them — binaries never touch
|
||||
* node_modules at runtime) and the interned host-call adapters that let the
|
||||
* engine invoke scriptc closures. */
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import type { CEmitter } from "./c-emitter.js";
|
||||
import { cDecl, cFnPtrCast, cStringLiteral, releaseCallC } from "./types.js";
|
||||
import { type IrType, islandCallbackRet, NPM_COMPRESS_MIN, typeKey } from "../../ir/ir.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
|
||||
/** Embedded npm modules (--dynamic): every reached module's SOURCE as a
|
||||
* static string plus the resolution edges — the island's module loader
|
||||
* and require shim read these tables; binaries never touch node_modules
|
||||
* at runtime. Registered with the runtime at the top of main.
|
||||
*
|
||||
* Module text at least NPM_COMPRESS_MIN long stores as raw DEFLATE when
|
||||
* that shrinks it (JS deflates 3-4×; a big CLI graph is tens of MB of
|
||||
* text, THE dominant binary-size term). The table row carries the
|
||||
* inflated length beside the stored one, and the island's loader inflates
|
||||
* LAZILY at a module's first load through the inflater the emitted main
|
||||
* installs (scr_island_set_inflate ← scr_zlib_inflate_exact; index.ts
|
||||
* links scr_zlib.c/libz on the same moduleEmbedsCompressedNpm predicate),
|
||||
* so boot touches no compressed page a run never loads — cold start
|
||||
* SHRINKS with the binary. */
|
||||
export function emitNpmEmbedding(emitter: CEmitter, out: string[]): void {
|
||||
const embedded = emitter.mod.embedded;
|
||||
if (embedded && embedded.modules.length > 0) {
|
||||
const emitChunked = (name: string, bytes: Buffer, comment?: string): void => {
|
||||
out.push(`static const char ${name}[] = ${comment !== undefined ? `/* ${comment} */` : ""}`);
|
||||
for (let off = 0; off < bytes.length; off += 2048) {
|
||||
const last = off + 2048 >= bytes.length;
|
||||
out.push(` ${cStringLiteral(bytes.subarray(off, off + 2048))}${last ? ";" : ""}`);
|
||||
}
|
||||
if (bytes.length === 0) out.push(` "";`);
|
||||
};
|
||||
// text → {bytes to store, raw: 0 for plain | the inflated length}.
|
||||
// The candidate test is moduleEmbedsCompressedNpm's, by construction.
|
||||
const store = (text: string): { bytes: Buffer; raw: number } => {
|
||||
const plain = Buffer.from(text, "utf8");
|
||||
if (text.length < NPM_COMPRESS_MIN) return { bytes: plain, raw: 0 };
|
||||
const deflated = deflateRawSync(plain, { level: 9 });
|
||||
return deflated.length < plain.length
|
||||
? { bytes: deflated, raw: plain.length }
|
||||
: { bytes: plain, raw: 0 };
|
||||
};
|
||||
const stored = embedded.modules.map((m) => ({
|
||||
src: store(m.source),
|
||||
esm: m.esm !== undefined ? store(m.esm) : null,
|
||||
}));
|
||||
stored.forEach((s, i) => {
|
||||
const m = embedded.modules[i]!;
|
||||
emitChunked(`sc_npm_src_${i}`, s.src.bytes, m.key.split("/node_modules/").pop());
|
||||
// CJS modules carry their synthesized ESM facade (default plus the
|
||||
// export names LEXED at build time, matching Node's vendored CJS
|
||||
// lexer): the island loader evaluates it when an ES module imports
|
||||
// the file.
|
||||
if (s.esm !== null) emitChunked(`sc_npm_esm_${i}`, s.esm.bytes);
|
||||
});
|
||||
const fmt = { esm: 0, cjs: 1, json: 2 } as const;
|
||||
out.push(`static const ScrIslandModule sc_npm_modules[] = {`);
|
||||
stored.forEach((s, i) => {
|
||||
const m = embedded.modules[i]!;
|
||||
const facade =
|
||||
s.esm !== null
|
||||
? `sc_npm_esm_${i}, sizeof sc_npm_esm_${i} - 1, ${s.esm.raw}`
|
||||
: `NULL, 0, 0`;
|
||||
out.push(
|
||||
` { ${cStringLiteral(Buffer.from(m.key, "utf8"))}, sc_npm_src_${i}, ` +
|
||||
`sizeof sc_npm_src_${i} - 1, ${s.src.raw}, ${fmt[m.format]}, ${facade} },`,
|
||||
);
|
||||
});
|
||||
out.push(`};`);
|
||||
if (embedded.edges.length > 0) {
|
||||
// kind: 0 = any call form, 1 = import-resolved, 2 = require-resolved
|
||||
// (a dual package's condition split — the loader looks edges up with
|
||||
// its call form's kind).
|
||||
const kindOf = { any: 0, import: 1, require: 2 } as const;
|
||||
out.push(`static const ScrIslandEdge sc_npm_edges[] = {`);
|
||||
for (const e of embedded.edges) {
|
||||
const s = (t: string) => cStringLiteral(Buffer.from(t, "utf8"));
|
||||
out.push(` { ${s(e.from)}, ${s(e.specifier)}, ${s(e.to)}, ${kindOf[e.kind]} },`);
|
||||
}
|
||||
out.push(`};`);
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
}
|
||||
|
||||
/** The host-call adapter for closures of a given (arity, return kind) —
|
||||
* see islandAdapters. `argv` cells are BORROWED by the wrapper; the
|
||||
* closure ABI consumes (+1) each param, so the adapter retains them in.
|
||||
* A jsval-returning closure's +1 result passes straight through;
|
||||
* primitive returns marshal back by value; void closures return NULL
|
||||
* (the wrapper turns that into `undefined`). */
|
||||
export function islandAdapter(emitter: CEmitter, arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string {
|
||||
const tag = { void: "v", jsval: "j", f64: "f", bool: "b", string: "s" }[retKind];
|
||||
const key = `${arity}:${tag}`;
|
||||
const existing = emitter.islandAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_ia_${arity}${tag}`;
|
||||
emitter.islandAdapters.set(key, name);
|
||||
const cRet = { void: "void", jsval: "ScrJsval *", f64: "double", bool: "bool", string: "ScrStr *" }[retKind];
|
||||
const params = ["ScrClosure *"].concat(Array<string>(arity).fill("ScrJsval *"));
|
||||
const callArgs = ["c", ...Array.from({ length: arity }, (_, i) => `scr_jsval_retain(argv[${i}])`)];
|
||||
const call = `((Fn)c->fn)(${callArgs.join(", ")})`;
|
||||
const body =
|
||||
retKind === "void"
|
||||
? [` ${call};`, ` return NULL;`]
|
||||
: retKind === "jsval"
|
||||
? [` return ${call};`]
|
||||
: retKind === "f64"
|
||||
? [` return scr_jsval_from_f64(${call});`]
|
||||
: retKind === "bool"
|
||||
? [` return scr_jsval_from_bool(${call});`]
|
||||
: [
|
||||
// The closure's +1 string marshals in, then releases.
|
||||
// NULL is the throw-path dummy — the wrapper sees the
|
||||
// pending exception and reverse-bridges it.
|
||||
` ScrStr *s = ${call};`,
|
||||
` if (!s) return NULL;`,
|
||||
` ScrJsval *r = scr_jsval_from_str(s);`,
|
||||
` scr_str_release(s);`,
|
||||
` return r;`,
|
||||
];
|
||||
const sig = `static ScrJsval *${name}(ScrClosure *c, ScrJsval **argv)`;
|
||||
emitter.walkerProtos.push(
|
||||
`${sig}; /* island host-call adapter (${arity} arg${arity === 1 ? "" : "s"}, ${retKind}) */`,
|
||||
);
|
||||
emitter.walkerDefs.push(
|
||||
`${sig} {`,
|
||||
` typedef ${cRet} (*Fn)(${params.join(", ")});`,
|
||||
...(arity === 0 ? [` (void)argv;`] : []),
|
||||
...body,
|
||||
`}`,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The host-call adapter for a closure with TYPED parameters — one per
|
||||
* full signature (param typeKeys + return classification), interned like
|
||||
* the all-'any' adapters above. On invocation from the engine, each
|
||||
* BORROWED argv cell converts to its param's static type through the
|
||||
* EXISTING exit machinery: strict primitive exits, JSON round-trip
|
||||
* composites via the dynCheck walker (width-tolerant records, path-
|
||||
* annotated failures), a `T | undefined` param taking the interned
|
||||
* undefined arm when the argument is absent or undefined (the engine
|
||||
* wrapper pads missing args with undefined and drops surplus — JS call
|
||||
* semantics), and jsval params passing through as retained handles. A
|
||||
* conversion failure releases what was already built and returns NULL
|
||||
* with the exception pending — the wrapper reverse-bridges it into the
|
||||
* island as a TypeError, so a lying engine argument throws instead of
|
||||
* corrupting memory (the boundary's trust-but-verify rule). Converted
|
||||
* params are CONSUMED by the closure ABI (+1 each moves in). Sync
|
||||
* returns marshal back like the all-'any' adapters; a Promise return
|
||||
* (async callbacks) wraps as an engine promise settled when the scriptc
|
||||
* promise settles (scr_jsval_from_promise). */
|
||||
export function islandTypedAdapter(emitter: CEmitter, fn: IrType & { kind: "func" }): string {
|
||||
const ret = islandCallbackRet(fn.ret, (id) => emitter.recordsById.get(id), (id) => emitter.unionsById.get(id));
|
||||
if (!ret) throw new InternalCompilerError("emitter bug: typed island adapter with unsupported return");
|
||||
const key = `${fn.params.map((p) => typeKey(p)).join(",")}=>${ret.async ? "P:" : ""}${ret.tag}`;
|
||||
const existing = emitter.islandTypedAdapters.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `sc_ita_${emitter.islandTypedAdapters.size}`;
|
||||
emitter.islandTypedAdapters.set(key, name);
|
||||
const decls: string[] = [];
|
||||
const conv: string[] = [];
|
||||
const cleanup: string[] = [];
|
||||
let canFail = false;
|
||||
fn.params.forEach((p, i) => {
|
||||
const a = `sc_a${i}`;
|
||||
switch (p.kind) {
|
||||
case "dyn":
|
||||
decls.push(` ScrDyn *${a} = NULL;`);
|
||||
conv.push(` ${a} = scr_dyn_from_jsval(argv[${i}]);`);
|
||||
cleanup.push(` scr_dyn_release(${a});`);
|
||||
break;
|
||||
case "jsval":
|
||||
decls.push(` ScrJsval *${a} = NULL;`);
|
||||
conv.push(` ${a} = scr_jsval_retain(argv[${i}]);`);
|
||||
cleanup.push(` scr_jsval_release(${a});`);
|
||||
break;
|
||||
case "f64":
|
||||
canFail = true;
|
||||
decls.push(` double ${a} = 0;`);
|
||||
conv.push(` if (!scr_jsval_exit_f64(argv[${i}], &${a})) goto sc_convfail;`);
|
||||
break;
|
||||
case "bool":
|
||||
canFail = true;
|
||||
decls.push(` bool ${a} = false;`);
|
||||
conv.push(` if (!scr_jsval_exit_bool(argv[${i}], &${a})) goto sc_convfail;`);
|
||||
break;
|
||||
case "string":
|
||||
canFail = true;
|
||||
decls.push(` ScrStr *${a} = NULL;`);
|
||||
conv.push(` ${a} = scr_jsval_exit_str(argv[${i}]);`, ` if (!${a}) goto sc_convfail;`);
|
||||
cleanup.push(` scr_str_release(${a});`);
|
||||
break;
|
||||
default: {
|
||||
// Composite (record/array/union): the jsExit pipeline — engine
|
||||
// JSON.stringify, json.parse, the interned dynCheck builder.
|
||||
canFail = true;
|
||||
const utag = undefinedArmTag(p, emitter.unionsById);
|
||||
decls.push(` ${cDecl(p, a)} = NULL;`);
|
||||
const roundTrip = [
|
||||
` ScrStr *sc_j = scr_jsval_to_json(argv[${i}]);`,
|
||||
` if (!sc_j) goto sc_convfail;`,
|
||||
` ScrDyn *sc_d = scr_json_parse(sc_j);`,
|
||||
` scr_str_release(sc_j);`,
|
||||
` if (!sc_d) goto sc_convfail;`,
|
||||
` ${a} = ${emitter.dynCheckHelper(p)}(sc_d, NULL);`,
|
||||
` scr_dyn_release(sc_d);`,
|
||||
` if (scr_exc_pending()) goto sc_convfail;`,
|
||||
];
|
||||
if (p.kind === "union" && utag >= 0) {
|
||||
conv.push(
|
||||
` if (scr_jsval_is_undefined(argv[${i}])) {`,
|
||||
` ${a} = ${emitter.unitInstanceRef(p.unionId, utag)}; /* absent/undefined argument -> the undefined arm */`,
|
||||
` } else {`,
|
||||
...roundTrip,
|
||||
` }`,
|
||||
);
|
||||
} else {
|
||||
conv.push(` {`, ...roundTrip, ` }`);
|
||||
}
|
||||
cleanup.push(` ${releaseCallC(p, a)};`);
|
||||
}
|
||||
}
|
||||
});
|
||||
const call = `(${cFnPtrCast(fn)}c->fn)(${["c", ...fn.params.map((_, i) => `sc_a${i}`)].join(", ")})`;
|
||||
const body: string[] = [];
|
||||
if (ret.async) {
|
||||
// The closure returns a +1 ScrPromise (async bodies spawn eagerly to
|
||||
// the first suspension and return it; a sync body building a promise
|
||||
// may throw instead — NULL dummy with the exception pending, which
|
||||
// the wrapper bridges). from_promise takes ownership.
|
||||
const tagC = {
|
||||
void: "SCR_ISLP_VOID", f64: "SCR_ISLP_F64", bool: "SCR_ISLP_BOOL",
|
||||
string: "SCR_ISLP_STR", jsval: "SCR_ISLP_JSVAL",
|
||||
json: "", dyn: "", // unreachable: islandCallbackRet never tags async json/dyn
|
||||
}[ret.tag];
|
||||
body.push(
|
||||
` ScrPromise *sc_r = ${call};`,
|
||||
` if (!sc_r) return NULL;`,
|
||||
` return scr_jsval_from_promise(sc_r, ${tagC});`,
|
||||
);
|
||||
} else {
|
||||
switch (ret.tag) {
|
||||
case "void":
|
||||
body.push(` ${call};`, ` return NULL;`);
|
||||
break;
|
||||
case "jsval":
|
||||
body.push(` return ${call};`);
|
||||
break;
|
||||
case "f64":
|
||||
body.push(` return scr_jsval_from_f64(${call});`);
|
||||
break;
|
||||
case "bool":
|
||||
body.push(` return scr_jsval_from_bool(${call});`);
|
||||
break;
|
||||
case "string":
|
||||
body.push(
|
||||
// The closure's +1 string marshals in, then releases. NULL is
|
||||
// the throw-path dummy — the wrapper bridges the pending
|
||||
// exception.
|
||||
` ScrStr *sc_s = ${call};`,
|
||||
` if (!sc_s) return NULL;`,
|
||||
` ScrJsval *sc_r = scr_jsval_from_str(sc_s);`,
|
||||
` scr_str_release(sc_s);`,
|
||||
` return sc_r;`,
|
||||
);
|
||||
break;
|
||||
case "dyn":
|
||||
body.push(
|
||||
// A checked-dynamic (+1 dyn) result: deep copy into the
|
||||
// engine (the jsMarshal dyn rule — data kinds only; boxed
|
||||
// functions/handles leave the exception pending and the NULL
|
||||
// dummy bridges).
|
||||
` ScrDyn *sc_d = ${call};`,
|
||||
` if (!sc_d) return NULL;`,
|
||||
` ScrJsval *sc_r = scr_jsval_from_dyn(sc_d);`,
|
||||
` scr_dyn_release(sc_d);`,
|
||||
` return sc_r;`,
|
||||
);
|
||||
break;
|
||||
case "json": {
|
||||
// A JSON-safe composite return: the jsMarshal path — the
|
||||
// type-directed serializer, then the engine's JSON parser (deep
|
||||
// copy; the documented aliasing divergence). NULL result is the
|
||||
// throw-path dummy.
|
||||
const helper = emitter.jsonWriteHelper(fn.ret);
|
||||
body.push(
|
||||
` ${cDecl(fn.ret, "sc_rv")} = ${call};`,
|
||||
` if (scr_exc_pending()) { ${releaseCallC(fn.ret, "sc_rv")}; return NULL; }`,
|
||||
` ScrJsonBuf sc_jb; scr_jb_init(&sc_jb);`,
|
||||
` ${helper}(&sc_jb, sc_rv);`,
|
||||
` ${releaseCallC(fn.ret, "sc_rv")};`,
|
||||
` ScrStr *sc_rj = scr_jb_finish(&sc_jb);`,
|
||||
` ScrJsval *sc_r = scr_jsval_from_json(sc_rj);`,
|
||||
` scr_str_release(sc_rj);`,
|
||||
` return sc_r;`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const fail = canFail
|
||||
? [
|
||||
`sc_convfail:`,
|
||||
// Params already converted release here (NULL-tolerant; unit-arm
|
||||
// instances are immortal — their release is a no-op); the ones
|
||||
// never reached are still NULL/scalar. The pending TypeError
|
||||
// reverse-bridges in the wrapper.
|
||||
...cleanup,
|
||||
` return NULL;`,
|
||||
]
|
||||
: [];
|
||||
const sig = `static ScrJsval *${name}(ScrClosure *c, ScrJsval **argv)`;
|
||||
emitter.walkerProtos.push(`${sig}; /* typed island host-call adapter: ${key} */`);
|
||||
emitter.walkerDefs.push(
|
||||
`${sig} { /* ${key} */`,
|
||||
...(fn.params.length === 0 ? [` (void)argv;`] : []),
|
||||
...decls,
|
||||
...conv,
|
||||
...body,
|
||||
...fail,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
@@ -1,838 +0,0 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Per-shape C emission: class/record struct definitions and their RC/trace
|
||||
* helper families, the hierarchy vtable machinery (slot structs, per-class
|
||||
* instances, exact-signature adapter thunks), and the capture-box
|
||||
* constructors. Everything here is driven by the class graph (ClassMeta,
|
||||
* VtSlot) the emitter builds up front; emission ORDER is part of the C. */
|
||||
import type { CEmitter } from "./c-emitter.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
import { type IrClassDef, type IrType, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, isRefCounted, mapOf, STRING } from "../../ir/ir.js";
|
||||
import { mangleClassGcFree, mangleClassNew, mangleClassRelease, mangleClassReleaseDirect, mangleClassRetain, mangleClassStruct, mangleClassTrace, mangleCtorThunk, mangleField, mangleFunction, mangleRecordClone, mangleRecordGcFree, mangleRecordNew, mangleRecordRelease, mangleRecordRetain, mangleRecordStruct, mangleRecordTrace, mangleVtAdapter, mangleVtInstance, mangleVtStruct } from "../mangle.js";
|
||||
import { boxKindC, cCommentText, cDecl, cType, elemKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
|
||||
import { streamRooted } from "../../ir/analysis.js";
|
||||
|
||||
/** The overflow map's C member name on index-signature record structs.
|
||||
* User fields mangle to `sc_fld_*`, so no field can collide. */
|
||||
export const OVERFLOW_MEMBER = "sc_ovf";
|
||||
|
||||
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
|
||||
* owns the slot; its declaration's IrFunction fixes the slot's C signature
|
||||
* (`this` typed as the declarer — implementations sit behind reinterpreting
|
||||
* adapters). Only methods overridden somewhere get slots. */
|
||||
export interface VtSlot {
|
||||
method: string;
|
||||
declarer: ClassMeta;
|
||||
fn: IrFunction;
|
||||
/** The slot's C member name, unique WITHIN its root's vtable struct:
|
||||
* sibling branches can each own a slot for the same method name
|
||||
* (mangleVtSlot's occurrence ordinal disambiguates). */
|
||||
member: string;
|
||||
}
|
||||
|
||||
/** Per-class node of the class graph (see CEmitter.classMeta). `pre`/`post`
|
||||
* are the preorder interval over the whole-program class forest — a class's
|
||||
* descendants are exactly the classes whose `pre` lies inside it, which is
|
||||
* both the instanceof check and the slot-lookup subtree test. */
|
||||
export class ClassMeta {
|
||||
def: IrClassDef;
|
||||
base: ClassMeta | null = null;
|
||||
children: ClassMeta[] = [];
|
||||
root: ClassMeta;
|
||||
pre = 0;
|
||||
post = 0;
|
||||
hierarchy = false;
|
||||
/** Root classes: the hierarchy's slots in DFS-declaration order. */
|
||||
slots: VtSlot[] = [];
|
||||
|
||||
constructor(def: IrClassDef) {
|
||||
this.def = def;
|
||||
// Every node is a valid root until the forest is linked and numbered.
|
||||
// No asserted undefined value crosses into native record storage.
|
||||
this.root = this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface StructShape {
|
||||
struct: string;
|
||||
newFn: string;
|
||||
retain: string;
|
||||
release: string;
|
||||
trace: string;
|
||||
gcFree: string;
|
||||
traced: boolean;
|
||||
fields: { name: string; type: IrType }[];
|
||||
/** Records with a string index signature: the overflow map's VALUE
|
||||
* type. The struct carries a trailing `ScrMap *` member the shape's
|
||||
* new/release/trace treat as one more (map-typed) field. */
|
||||
indexValue?: IrType;
|
||||
comment: string;
|
||||
/** Record shape id; absent for classes. */
|
||||
recordId?: string;
|
||||
/** Class shapes only; hierarchy members get the vtable machinery. */
|
||||
meta: ClassMeta | null;
|
||||
}
|
||||
|
||||
/** Per-shape C structs + RC helpers for classes AND record shapes (the
|
||||
* layouts are identical: `size_t rc` header + fields). All structs are
|
||||
* forward-declared first so fields can reference any shape regardless of
|
||||
* order (classes nesting records, records nesting classes, mutual
|
||||
* references), and the RC helpers are prototyped before any body so a
|
||||
* release can call another shape's release regardless of emission order.
|
||||
* The `_v` adapters give boxes untyped RC entry points without UB casts.
|
||||
*
|
||||
* Cycle-capable shapes (the constructor fixpoint) additionally get a
|
||||
* cycle header (scr_cyc_alloc) plus two collector entry points: a trace
|
||||
* visiting exactly the cycle-capable fields, and a teardown releasing
|
||||
* exactly the other refcounted fields (the trace/teardown complement
|
||||
* contract in scr_runtime.h) — and their retain/release feed the
|
||||
* candidate-root buffer. Acyclic shapes keep the lean 1-word header. */
|
||||
export function emitStructDefs(emitter: CEmitter, out: string[]): void {
|
||||
// Runtime-provided classes (the builtin Error hierarchy) emit NOTHING
|
||||
// here — struct, RC helpers, and vtables live in the runtime. They keep
|
||||
// their ClassMeta (preorder numbering, instanceof constants, vtable
|
||||
// struct type for user subclasses); main() stamps their intervals.
|
||||
const shapes: StructShape[] = [
|
||||
...(emitter.mod.classes ?? []).filter((cls) => !cls.runtime).map((cls): StructShape => ({
|
||||
struct: mangleClassStruct(cls.name),
|
||||
newFn: mangleClassNew(cls.name),
|
||||
retain: mangleClassRetain(cls.name),
|
||||
release: mangleClassRelease(cls.name),
|
||||
trace: mangleClassTrace(cls.name),
|
||||
gcFree: mangleClassGcFree(cls.name),
|
||||
traced: emitter.tracedShapes.has(`object:${cls.name}`),
|
||||
fields: cls.fields,
|
||||
comment: `class ${cls.name}`,
|
||||
meta: emitter.classMeta.get(cls.name) ?? null,
|
||||
})),
|
||||
...(emitter.mod.records ?? []).map((rec): StructShape => ({
|
||||
struct: mangleRecordStruct(rec.id),
|
||||
newFn: mangleRecordNew(rec.id),
|
||||
retain: mangleRecordRetain(rec.id),
|
||||
release: mangleRecordRelease(rec.id),
|
||||
trace: mangleRecordTrace(rec.id),
|
||||
gcFree: mangleRecordGcFree(rec.id),
|
||||
traced: emitter.tracedShapes.has(`record:${rec.id}`),
|
||||
fields: rec.fields,
|
||||
recordId: rec.id,
|
||||
...(rec.indexValue ? { indexValue: rec.indexValue } : {}),
|
||||
comment: `record ${rec.id} { ${rec.fields.map((f) => f.name).join("; ")}${rec.indexValue ? "; [key: string]" : ""} }`,
|
||||
meta: null,
|
||||
})),
|
||||
];
|
||||
if (shapes.length === 0) return;
|
||||
const inHierarchy = (s: StructShape): boolean => s.meta !== null && s.meta.hierarchy;
|
||||
// The RC-relevant members of a shape: every field, plus the overflow
|
||||
// map on index-signature shapes — release/trace/teardown treat it as
|
||||
// one more map-typed member (traceAdapterC's map rule answers whether
|
||||
// the record's trace must visit it).
|
||||
const rcMembers = (s: StructShape): { member: string; type: IrType; name: string }[] => [
|
||||
...s.fields.map((f) => ({ member: mangleField(f.name), type: f.type, name: f.name })),
|
||||
...(s.meta?.def.localCaptures !== undefined
|
||||
? [{ member: "sc_class", type: { kind: "classval" as const, className: s.meta.def.name }, name: "class environment" }]
|
||||
: []),
|
||||
...(s.indexValue
|
||||
? [{ member: OVERFLOW_MEMBER, type: mapOf(STRING, s.indexValue), name: "[key: string] overflow" }]
|
||||
: []),
|
||||
];
|
||||
// CLASS newFns start every undefined-armed union field at JS's
|
||||
// `undefined` — the interned immortal unit instance — instead of the
|
||||
// calloc NULL: tsc's strictPropertyInitialization accepts such fields
|
||||
// with no initializer and no constructor assignment (undefined is in
|
||||
// the type), so a fresh instance is readable before any assignment
|
||||
// runs — a method assigns later, a constructor branch skips it, a base
|
||||
// constructor's virtual call reads a derived field before super()
|
||||
// returns. Node reads `undefined` there; a NULL ScrUnion would be a
|
||||
// segfault. Class shapes only: record shapes are fully written at
|
||||
// every construction site (literal lowering fills omitted optional
|
||||
// fields, the dynCheck/JSON builders fill missing keys), and the
|
||||
// immortal instance costs nothing if overwritten (releases skip it).
|
||||
const undefFieldInitC = (s: StructShape): string[] =>
|
||||
s.meta === null ? [] : s.fields.flatMap((f) => emitter.undefFieldInitLineC(f.name, f.type));
|
||||
// The overflow map's construction call (in the shape's newFn): value
|
||||
// handling is type-directed exactly like a user Map's.
|
||||
const overflowNewC = (s: StructShape): string => {
|
||||
const v = s.indexValue!;
|
||||
const valKind = mapValKindC(v);
|
||||
if (valKind !== "SCR_MAP_VAL_REF") {
|
||||
return `scr_map_new(SCR_MAP_KEY_STR, ${valKind}, NULL, NULL, NULL)`;
|
||||
}
|
||||
const rc = vAdapters(v);
|
||||
return `scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_REF, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(v)})`;
|
||||
};
|
||||
|
||||
for (const s of shapes) {
|
||||
out.push(`typedef struct ${s.struct} ${s.struct}; /* ${cCommentText(s.comment)} */`);
|
||||
}
|
||||
out.push("");
|
||||
for (const s of shapes) {
|
||||
out.push(`struct ${s.struct} { /* ${cCommentText(s.comment)} */`, ` size_t rc;`);
|
||||
if (s.meta?.def.localCaptures !== undefined) out.push(` ScrClassObj *sc_class;`);
|
||||
if (inHierarchy(s)) {
|
||||
// The hierarchy prefix: base fields follow at identical offsets in
|
||||
// every subclass, so vt must sit between rc and the field list.
|
||||
out.push(` const ScrVt *vt;`);
|
||||
if (s.meta!.root.def.name === RUNTIME_EMITTER_CLASS) {
|
||||
// Emitter subclasses embed ScrEmitter's remaining prefix (the
|
||||
// registry and display-name slots) so an upcast to ScrEmitter*
|
||||
// is the usual pointer reinterpret. Carried by the BACKEND —
|
||||
// the IR field lists stay empty for it.
|
||||
out.push(
|
||||
` ScrEeReg *sc_eereg; /* EventEmitter registry (ScrEmitter prefix) */`,
|
||||
` const char *sc_eecls; /* EventEmitter display name (ScrEmitter prefix) */`,
|
||||
);
|
||||
if (streamRooted(s.meta!)) {
|
||||
// Stream subclasses embed ScrStream's remaining slot: the
|
||||
// state pointer, NULL until the constructor's super(options)
|
||||
// reaches scr_stream_init_* — so an upcast to ScrStream* (and
|
||||
// on through ScrEmitter*) is the usual pointer reinterpret.
|
||||
out.push(` ScrStreamState *sc_st; /* stream state (ScrStream prefix) */`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const f of s.fields) {
|
||||
out.push(` ${cDecl(f.type, mangleField(f.name))}; /* ${cCommentText(f.name)} */`);
|
||||
}
|
||||
if (s.indexValue) {
|
||||
out.push(` ScrMap *${OVERFLOW_MEMBER}; /* [key: string] overflow (string-keyed) */`);
|
||||
}
|
||||
out.push(`};`);
|
||||
}
|
||||
out.push("");
|
||||
// Vtable struct typedefs are named after hierarchy ROOTS, which may be
|
||||
// a runtime error class (a user `extends Error` subclass's vtable
|
||||
// instance is typed by the %Error root's struct) — so typedefs come
|
||||
// from the EMITTED hierarchy classes' roots, while adapter prototypes,
|
||||
// instances, and helpers stay shapes-only (the runtime owns the
|
||||
// builtin classes; a subclass-free Error tree emits nothing at all).
|
||||
emitter.emitVtableDecls(out, shapes.filter(inHierarchy).map((s) => s.meta!));
|
||||
for (const s of shapes) {
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o);`,
|
||||
`static void ${s.release}(${s.struct} *o);`,
|
||||
`static ${s.struct} *${s.newFn}(void);`,
|
||||
);
|
||||
if (s.recordId !== undefined && emitter.recordCloneShapes.has(s.recordId)) {
|
||||
out.push(`static ${s.struct} *${mangleRecordClone(s.recordId)}(${s.struct} *src);`);
|
||||
}
|
||||
if (inHierarchy(s)) {
|
||||
out.push(`static void ${mangleClassReleaseDirect(s.meta!.def.name)}(void *o0);`);
|
||||
}
|
||||
if (s.traced) {
|
||||
out.push(
|
||||
`static void ${s.trace}(void *o, ScrTraceVisit visit, void *ctx);`,
|
||||
`static void ${s.gcFree}(void *o);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
emitter.emitVtableInstances(out, shapes.filter(inHierarchy).map((s) => s.meta!));
|
||||
for (const s of shapes) {
|
||||
if (inHierarchy(s)) {
|
||||
emitter.emitHierarchyClassHelpers(out, s.meta!, s);
|
||||
continue;
|
||||
}
|
||||
// NULL-tolerant: zeroed fields (calloc) and user `null as unknown as C`
|
||||
// casts can put NULL where an object is expected.
|
||||
if (!s.traced) {
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
` if (o && o->rc != SIZE_MAX) o->rc++;`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
);
|
||||
for (const m of rcMembers(s)) {
|
||||
if (!isRefCounted(m.type)) continue;
|
||||
const field = `o->${m.member}`;
|
||||
out.push(` if (${field}) ${releaseCallC(m.type, field)};`);
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
` free(o);`,
|
||||
` }`,
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
` ${s.struct} *o = calloc(1, sizeof *o);`,
|
||||
` if (!o) { scr_trap("scriptc: out of memory\\n"); }`,
|
||||
` o->rc = 1;`,
|
||||
...undefFieldInitC(s),
|
||||
...(s.indexValue ? [` o->${OVERFLOW_MEMBER} = ${overflowNewC(s)};`] : []),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
...(s.recordId !== undefined && emitter.recordCloneShapes.has(s.recordId)
|
||||
? emitRecordCloneC(s.recordId, s.struct, s.fields, s.newFn)
|
||||
: []),
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Cycle-capable shape: cycle-headered allocation, root-buffer hooks
|
||||
// on release, and the trace/teardown complement pair.
|
||||
const tracedFields = rcMembers(s).filter((m) => emitter.traceAdapterC(m.type) !== null);
|
||||
const untracedRefFields = rcMembers(s).filter(
|
||||
(m) => isRefCounted(m.type) && emitter.traceAdapterC(m.type) === null,
|
||||
);
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
` if (o && o->rc != SIZE_MAX) {`,
|
||||
` o->rc++;`,
|
||||
` scr_cyc_mark_live(o);`,
|
||||
` }`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
` scr_cyc_on_dead(o);`,
|
||||
);
|
||||
for (const m of rcMembers(s)) {
|
||||
if (!isRefCounted(m.type)) continue;
|
||||
const field = `o->${m.member}`;
|
||||
out.push(` if (${field}) ${releaseCallC(m.type, field)};`);
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o);`,
|
||||
` } else {`,
|
||||
` scr_cyc_on_release(o); /* possible cycle root; may collect */`,
|
||||
` }`,
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
` ${s.struct} *o = scr_cyc_alloc(sizeof *o, &${s.trace}, &${s.gcFree});`,
|
||||
` o->rc = 1;`,
|
||||
...undefFieldInitC(s),
|
||||
...(s.indexValue ? [` o->${OVERFLOW_MEMBER} = ${overflowNewC(s)};`] : []),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
...(s.recordId !== undefined && emitter.recordCloneShapes.has(s.recordId)
|
||||
? emitRecordCloneC(s.recordId, s.struct, s.fields, s.newFn)
|
||||
: []),
|
||||
`static void ${s.trace}(void *o0, ScrTraceVisit visit, void *ctx) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...tracedFields.map(
|
||||
(m) => ` visit(o->${m.member}, ctx); /* ${cCommentText(m.name)} */`,
|
||||
),
|
||||
`}`,
|
||||
`static void ${s.gcFree}(void *o0) {`,
|
||||
...(untracedRefFields.length > 0
|
||||
? [
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...untracedRefFields.map((m) => {
|
||||
const field = `o->${m.member}`;
|
||||
return ` if (${field}) ${releaseCallC(m.type, field)}; /* ${cCommentText(m.name)} (acyclic) */`;
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o0);`,
|
||||
`}`,
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** One deliberately non-inlined same-shape clone helper. Keeping the copy
|
||||
* here makes each `{ ...largeRecord, override }` site constant-sized while
|
||||
* preserving the ordinary type-directed retain rules. */
|
||||
function emitRecordCloneC(
|
||||
shapeId: string,
|
||||
struct: string,
|
||||
fields: { name: string; type: IrType }[],
|
||||
newFn: string,
|
||||
): string[] {
|
||||
const noinline = fields.length >= 16;
|
||||
return [
|
||||
...(noinline
|
||||
? [
|
||||
`#if defined(_MSC_VER)`,
|
||||
`__declspec(noinline)`,
|
||||
`#else`,
|
||||
`__attribute__((noinline))`,
|
||||
`#endif`,
|
||||
]
|
||||
: []),
|
||||
`static ${struct} *${mangleRecordClone(shapeId)}(${struct} *src) {`,
|
||||
` ${struct} *o = ${newFn}();`,
|
||||
...fields.map((f) => {
|
||||
const member = mangleField(f.name);
|
||||
return ` o->${member} = ${isRefCounted(f.type) ? retainCallC(f.type, `src->${member}`) : `src->${member}`};`;
|
||||
}),
|
||||
` return o;`,
|
||||
`}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The root's slot list as seen by one class: the implementation the
|
||||
* class dispatches to, or null outside the slot's declaring subtree (a
|
||||
* call through this class's vtable can never reach that slot). An
|
||||
* ABSTRACT class whose chain holds only abstract declarations of the
|
||||
* slot also answers null — the class never instantiates (tsc), so its
|
||||
* own vtable entry can never dispatch. */
|
||||
export function vtEntriesFor(emitter: CEmitter, meta: ClassMeta): { slot: VtSlot; impl: ClassMeta | null }[] {
|
||||
return meta.root.slots.map((slot): { slot: VtSlot; impl: ClassMeta | null } => {
|
||||
if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) {
|
||||
return { slot, impl: null };
|
||||
}
|
||||
for (let c: ClassMeta | null = meta; c; c = c.base) {
|
||||
if (c.def.methods?.includes(slot.method) && !c.def.abstractMethods?.includes(slot.method)) {
|
||||
return { slot, impl: c };
|
||||
}
|
||||
}
|
||||
if (meta.def.abstract === true) return { slot, impl: null };
|
||||
throw new InternalCompilerError(`emitter bug: no implementation of ${slot.method} for ${meta.def.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** The C parameter list of a slot (declaring-class `this` first). */
|
||||
export function vtSlotParams(emitter: CEmitter, slot: VtSlot, named: boolean): string[] {
|
||||
const thisParam = named
|
||||
? `${mangleClassStruct(slot.declarer.def.name)} *o`
|
||||
: `${mangleClassStruct(slot.declarer.def.name)} *`;
|
||||
const rest = slot.fn.params
|
||||
.slice(1)
|
||||
.map((p, i) => (named ? cDecl(p.type, `sc_a${i}`) : cType(p.type).trim()));
|
||||
return [thisParam, ...rest];
|
||||
}
|
||||
|
||||
/** Vtable struct typedefs + adapter prototypes (the definitions call
|
||||
* function bodies, so they flush after the signature block — see emit). */
|
||||
export function emitVtableDecls(emitter: CEmitter, out: string[], hierarchyClasses: ClassMeta[]): void {
|
||||
// Roots of the EMITTED classes' hierarchies — a runtime root (%Error)
|
||||
// counts exactly when some emitted subclass needs its vtable type.
|
||||
const roots = [...new Set(hierarchyClasses.map((m) => m.root))];
|
||||
for (const root of roots) {
|
||||
const vtt = mangleVtStruct(root.def.name);
|
||||
out.push(`typedef struct ${vtt} { /* vtable: hierarchy rooted at ${root.def.name} */`);
|
||||
out.push(` ScrVt head;`);
|
||||
for (const slot of root.slots) {
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
out.push(
|
||||
` ${ret} (*${slot.member})(${emitter.vtSlotParams(slot, false).join(", ")}); /* ${cCommentText(slot.method)} */`,
|
||||
);
|
||||
}
|
||||
out.push(`} ${vtt};`);
|
||||
}
|
||||
if (roots.length > 0) out.push("");
|
||||
for (const meta of hierarchyClasses) {
|
||||
for (const { slot, impl } of emitter.vtEntriesFor(meta)) {
|
||||
if (impl === null) continue;
|
||||
const key = `${impl.def.name}.${slot.method}`;
|
||||
if (emitter.vtAdapters.has(key)) continue;
|
||||
emitter.vtAdapters.set(key, { impl, slot });
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
out.push(
|
||||
`static ${ret} ${mangleVtAdapter(impl.def.name, slot.method)}(${emitter.vtSlotParams(slot, false).join(", ")});`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (emitter.vtAdapters.size > 0) out.push("");
|
||||
}
|
||||
|
||||
/** One static const vtable per hierarchy class: interval, direct
|
||||
* release, and the class's dispatch entry for every slot. */
|
||||
export function emitVtableInstances(emitter: CEmitter, out: string[], hierarchyClasses: ClassMeta[]): void {
|
||||
for (const meta of hierarchyClasses) {
|
||||
const vtt = mangleVtStruct(meta.root.def.name);
|
||||
const head = `{ ${meta.pre}, ${meta.post}, &${mangleClassReleaseDirect(meta.def.name)} }`;
|
||||
const entries = emitter.vtEntriesFor(meta).map(({ slot, impl }) =>
|
||||
impl === null
|
||||
? `0 /* ${cCommentText(slot.method)}: outside the declaring subtree */`
|
||||
: `&${mangleVtAdapter(impl.def.name, slot.method)} /* ${cCommentText(slot.method)} */`,
|
||||
);
|
||||
out.push(
|
||||
`static const ${vtt} ${mangleVtInstance(meta.def.name)} = { /* class ${meta.def.name} */`,
|
||||
` ${[head, ...entries].join(",\n ")}`,
|
||||
`};`,
|
||||
);
|
||||
}
|
||||
if (hierarchyClasses.length > 0) out.push("");
|
||||
}
|
||||
|
||||
/** Adapter definitions (flushed after the signature block): the impl
|
||||
* class's method behind the slot's declaring-class signature. */
|
||||
export function emitVtAdapterDefs(emitter: CEmitter, out: string[]): void {
|
||||
for (const { impl, slot } of emitter.vtAdapters.values()) {
|
||||
const ret = cType(slot.fn.returnType).trim();
|
||||
const recv =
|
||||
impl === slot.declarer ? "o" : `(${mangleClassStruct(impl.def.name)} *)o`;
|
||||
const args = [recv, ...slot.fn.params.slice(1).map((_, i) => `sc_a${i}`)].join(", ");
|
||||
const call = `${mangleFunction(`%${impl.def.name}.${slot.method}`)}(${args})`;
|
||||
out.push(
|
||||
``,
|
||||
`static ${ret} ${mangleVtAdapter(impl.def.name, slot.method)}(${emitter.vtSlotParams(slot, true).join(", ")}) {`,
|
||||
slot.fn.returnType.kind === "void" ? ` ${call};` : ` return ${call};`,
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** RC helpers of one hierarchy class. Retain is layout-generic (rc sits
|
||||
* at offset 0 in every subclass); the PUBLIC release dispatches through
|
||||
* the object's vtable so a base-typed release tears down the derived
|
||||
* object; the DIRECT release (the vtable entry) does the class's own
|
||||
* teardown. Cycle capability is hierarchy-uniform (constructor fixpoint),
|
||||
* and the cycle header's trace/teardown are stamped with the concrete
|
||||
* class's functions at allocation — the collector needs no vtable. */
|
||||
export function emitHierarchyClassHelpers(emitter: CEmitter, out: string[],
|
||||
meta: ClassMeta,
|
||||
s: StructShape,): void {
|
||||
const reld = mangleClassReleaseDirect(meta.def.name);
|
||||
const emitterRooted = meta.root.def.name === RUNTIME_EMITTER_CLASS;
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
// The display name Node's leak warning prints ([My]) — the source
|
||||
// class name, without the module qualifier.
|
||||
const displayName = meta.def.name.includes(".")
|
||||
? meta.def.name.slice(meta.def.name.lastIndexOf(".") + 1)
|
||||
: meta.def.name;
|
||||
out.push(
|
||||
`static ${s.struct} *${s.retain}(${s.struct} *o) {`,
|
||||
...(s.traced
|
||||
? [` if (o && o->rc != SIZE_MAX) {`, ` o->rc++;`, ` scr_cyc_mark_live(o);`, ` }`]
|
||||
: [` if (o && o->rc != SIZE_MAX) o->rc++;`]),
|
||||
` return o;`,
|
||||
`}`,
|
||||
`static void ${s.release}(${s.struct} *o) {`,
|
||||
` if (!o || o->rc == SIZE_MAX) return;`,
|
||||
` o->vt->release(o); /* the DYNAMIC class's teardown */`,
|
||||
`}`,
|
||||
`static void ${reld}(void *o0) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
` if (--o->rc == 0) {`,
|
||||
...(s.traced ? [` scr_cyc_on_dead(o);`] : []),
|
||||
);
|
||||
for (const f of s.fields) {
|
||||
if (!isRefCounted(f.type)) continue;
|
||||
const field = `o->${mangleField(f.name)}`;
|
||||
out.push(` if (${field}) ${releaseCallC(f.type, field)};`);
|
||||
}
|
||||
if (emitterRooted) {
|
||||
out.push(` scr_emitter_reg_drop(o->sc_eereg); /* EventEmitter prefix */`);
|
||||
if (isStreamRooted) {
|
||||
out.push(` scr_stream_st_release(o->sc_st); /* stream state (ScrStream prefix) */`);
|
||||
}
|
||||
}
|
||||
out.push(
|
||||
` scr_obj_free_note();`,
|
||||
s.traced ? ` scr_cyc_free(o);` : ` free(o);`,
|
||||
...(s.traced
|
||||
? [` } else {`, ` scr_cyc_on_release(o); /* possible cycle root; may collect */`, ` }`]
|
||||
: [` }`]),
|
||||
`}`,
|
||||
`static ${s.struct} *${s.newFn}(void) {`,
|
||||
...(s.traced
|
||||
? [` ${s.struct} *o = scr_cyc_alloc(sizeof *o, &${s.trace}, &${s.gcFree});`]
|
||||
: [
|
||||
` ${s.struct} *o = calloc(1, sizeof *o);`,
|
||||
` if (!o) { scr_trap("scriptc: out of memory\\n"); }`,
|
||||
]),
|
||||
` o->rc = 1;`,
|
||||
` o->vt = &${mangleVtInstance(meta.def.name)}.head;`,
|
||||
...(emitterRooted
|
||||
? [` o->sc_eecls = ${JSON.stringify(displayName)}; /* EventEmitter prefix (reg stays NULL) */`]
|
||||
: []),
|
||||
// Undefined-admitting fields start as JS's undefined, not NULL — see
|
||||
// undefFieldInitLineC. s.fields is the FLATTENED layout (base prefix
|
||||
// + own), so a derived allocation covers inherited fields too.
|
||||
...s.fields.flatMap((f) => emitter.undefFieldInitLineC(f.name, f.type)),
|
||||
` scr_obj_alloc_note();`,
|
||||
` return o;`,
|
||||
`}`,
|
||||
);
|
||||
if (s.traced) {
|
||||
const tracedFields = s.fields.filter((f) => emitter.traceAdapterC(f.type) !== null);
|
||||
const untracedRefFields = s.fields.filter(
|
||||
(f) => isRefCounted(f.type) && emitter.traceAdapterC(f.type) === null,
|
||||
);
|
||||
out.push(
|
||||
`static void ${s.trace}(void *o0, ScrTraceVisit visit, void *ctx) {`,
|
||||
` ${s.struct} *o = (${s.struct} *)o0;`,
|
||||
...(emitterRooted
|
||||
? [` scr_emitter_reg_trace(o->sc_eereg, visit, ctx); /* listener closures */`]
|
||||
: []),
|
||||
...(isStreamRooted
|
||||
? [` scr_stream_st_trace(o->sc_st, visit, ctx); /* stream state closures/pipes */`]
|
||||
: []),
|
||||
...tracedFields.map((f) => ` visit(o->${mangleField(f.name)}, ctx); /* ${cCommentText(f.name)} */`),
|
||||
`}`,
|
||||
`static void ${s.gcFree}(void *o0) {`,
|
||||
...(untracedRefFields.length > 0 || emitterRooted
|
||||
? [` ${s.struct} *o = (${s.struct} *)o0;`]
|
||||
: []),
|
||||
...(emitterRooted
|
||||
? [` scr_emitter_reg_gcfree(o->sc_eereg); /* EventEmitter prefix */`]
|
||||
: []),
|
||||
...(isStreamRooted
|
||||
? [` scr_stream_st_gcfree(o->sc_st); /* stream state (ScrStream prefix) */`]
|
||||
: []),
|
||||
...untracedRefFields.map((f) => {
|
||||
const field = `o->${mangleField(f.name)}`;
|
||||
return ` if (${field}) ${releaseCallC(f.type, field)}; /* ${cCommentText(f.name)} (acyclic) */`;
|
||||
}),
|
||||
` scr_obj_free_note();`,
|
||||
` scr_cyc_free(o0);`,
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
out.push(
|
||||
`static void *${s.retain}_v(void *o) { return ${s.retain}((${s.struct} *)o); }`,
|
||||
`static void ${s.release}_v(void *o) { ${s.release}((${s.struct} *)o); }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** Class objects (classes as first-class values): one immortal
|
||||
* ScrClassObj static per class some classRef in the module names, plus
|
||||
* the construct-thunk PROTOTYPES their `ctor` slots take the address of
|
||||
* (definitions land later with the other synthesized bodies —
|
||||
* emitCtorThunkDefs). The interval constants are the same numbering the
|
||||
* vtables carry, so instanceOfValue agrees with compiled instanceOf. */
|
||||
export function emitClassObjs(emitter: CEmitter, out: string[]): void {
|
||||
if (emitter.classObjs.size === 0) return;
|
||||
out.push("");
|
||||
for (const [className, sym] of emitter.classObjs) {
|
||||
const meta = emitter.classMeta.get(className);
|
||||
if (!meta) throw new InternalCompilerError(`emitter bug: class object for unknown class ${className}`);
|
||||
// A generic-class INSTANTIATION's class object carries its FAMILY's
|
||||
// interval: at runtime JS has ONE `Box`, so instanceof through the
|
||||
// value must answer for the whole family (every instantiation and
|
||||
// their subclasses) — IrClassDef.genericOf. Construction still
|
||||
// dispatches the instantiation's own thunk.
|
||||
const intervalMeta = meta.def.genericOf !== undefined
|
||||
? emitter.classMeta.get(meta.def.genericOf)
|
||||
: meta;
|
||||
if (!intervalMeta) throw new InternalCompilerError(`emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`);
|
||||
const nameSym = emitter.internLiteral(meta.def.jsName ?? "");
|
||||
out.push(
|
||||
`static void *${mangleCtorThunk(className)}(ScrClassObj *sc_class${ctorThunkParams(emitter, className).decls ? ", " + ctorThunkParams(emitter, className).decls : ""});`,
|
||||
`static ScrClassObj ${sym} = { SIZE_MAX, ${intervalMeta.pre}, ${intervalMeta.post}, ` +
|
||||
`(void *)&${mangleCtorThunk(className)}, (const ScrStr *)&${nameSym}, 0, ${meta.def.jsLength ?? 0} }; /* class ${className} */`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** The construct thunk's parameter list: the constructor's completed ABI
|
||||
* minus the `this` the thunk allocates itself. */
|
||||
function ctorThunkParams(emitter: CEmitter, className: string): { decls: string; names: string[] } {
|
||||
const ctor = emitter.fnByName.get(`%${className}.constructor`);
|
||||
if (!ctor) throw new InternalCompilerError(`emitter bug: class object for ${className} without a constructor`);
|
||||
const params = ctor.params.slice(1);
|
||||
return {
|
||||
decls: params.map((p, i) => cDecl(p.type, `sc_a${i}`)).join(", "),
|
||||
names: params.map((_, i) => `sc_a${i}`),
|
||||
};
|
||||
}
|
||||
|
||||
/** Construct-thunk definitions (`void *sc_ct_C(args…)`): allocate, run
|
||||
* the constructor over a +1 `this` like the inline `new` emission, and
|
||||
* hand the remaining +1 out as `void *` — the one well-defined
|
||||
* function-pointer shape every class in a classval slot shares (the
|
||||
* frontend's ABI flow rule makes the parameter lists agree). A throwing
|
||||
* constructor leaves the pending flag set: the thunk releases the
|
||||
* half-built object and returns NULL (a dummy the checked call site
|
||||
* never reads). */
|
||||
export function emitCtorThunkDefs(emitter: CEmitter, out: string[]): void {
|
||||
for (const className of emitter.classObjs.keys()) {
|
||||
const { decls, names } = ctorThunkParams(emitter, className);
|
||||
const struct = mangleClassStruct(className);
|
||||
const lines = [
|
||||
``,
|
||||
`static void *${mangleCtorThunk(className)}(ScrClassObj *sc_class${decls ? ", " + decls : ""}) {`,
|
||||
` ${struct} *o = ${mangleClassNew(className)}();`,
|
||||
...(emitter.classMeta.get(className)?.def.localCaptures !== undefined
|
||||
? [` o->sc_class = scr_classobj_retain(sc_class);`]
|
||||
: [` (void)sc_class;`]),
|
||||
` ${mangleFunction(`%${className}.constructor`)}(${[`${mangleClassRetain(className)}(o)`, ...names].join(", ")});`,
|
||||
];
|
||||
if (emitter.mayThrow.has(`%${className}.constructor`)) {
|
||||
lines.push(
|
||||
` if (scr_exc_pending()) {`,
|
||||
` ${mangleClassRelease(className)}(o);`,
|
||||
` return NULL;`,
|
||||
` }`,
|
||||
);
|
||||
}
|
||||
lines.push(` return (void *)o;`, `}`);
|
||||
out.push(...lines);
|
||||
}
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime error vtables: preorder intervals
|
||||
* from THIS module's numbering, plus the traced-mode switch when the
|
||||
* cycle fixpoint marked the Error hierarchy (a user subclass holds
|
||||
* cycle-capable fields — capability is hierarchy-uniform, so the
|
||||
* runtime's own allocations need collector headers too). Empty for
|
||||
* hand-written IR without the builtin defs. */
|
||||
export function errorVtStampLines(emitter: CEmitter): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = emitter.classMeta.get(name);
|
||||
if (!meta) return [];
|
||||
lines.push(
|
||||
` scr_error_vts[${rec.kind}].pre = ${meta.pre}; scr_error_vts[${rec.kind}].post = ${meta.post}; /* ${rec.lib} */`,
|
||||
);
|
||||
}
|
||||
if (emitter.tracedShapes.has("object:%Error")) {
|
||||
lines.push(` scr_error_set_traced();`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime emitter vtable: its preorder
|
||||
* interval from THIS module's numbering (the errorVtStampLines story).
|
||||
* Empty when the program never touches the emitter surface — the class
|
||||
* def only rides modules that reference it. */
|
||||
export function emitterVtStampLines(emitter: CEmitter): string[] {
|
||||
const meta = emitter.classMeta.get(RUNTIME_EMITTER_CLASS);
|
||||
if (!meta) return [];
|
||||
return [
|
||||
` scr_emitter_vt.pre = ${meta.pre}; scr_emitter_vt.post = ${meta.post}; /* EventEmitter */`,
|
||||
];
|
||||
}
|
||||
|
||||
/** main()'s stamping of the runtime stream vtables (Readable/Writable/
|
||||
* Duplex/Transform/PassThrough) — the emitterVtStampLines story: each
|
||||
* def rides the module only when the program touches the stream
|
||||
* surface, and instanceof needs their preorder intervals under the
|
||||
* emitter root. */
|
||||
export function streamVtStampLines(emitter: CEmitter): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const [name, rec] of RUNTIME_STREAM_CLASSES) {
|
||||
const meta = emitter.classMeta.get(name);
|
||||
if (!meta) continue;
|
||||
const vt = `scr_${rec.lib.toLowerCase()}_vt`;
|
||||
lines.push(` ${vt}.pre = ${meta.pre}; ${vt}.post = ${meta.post}; /* ${rec.lib} */`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The trace entry point symbol for a payload/field type, or null when
|
||||
* the type cannot participate in a cycle (see the constructor fixpoint). */
|
||||
export function traceAdapterC(emitter: CEmitter, t: IrType): string | null {
|
||||
switch (t.kind) {
|
||||
case "dyn":
|
||||
return "scr_dyn_trace_v";
|
||||
case "classval":
|
||||
return "scr_classobj_trace_v";
|
||||
case "func":
|
||||
return "scr_closure_trace_v";
|
||||
case "union":
|
||||
return emitter.tracedUnions.has(t.unionId) ? "scr_union_trace_v" : null;
|
||||
case "promise":
|
||||
return "scr_promise_trace_v";
|
||||
case "object":
|
||||
if (!emitter.tracedShapes.has(`object:${t.className}`)) return null;
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) return "scr_error_trace";
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) return "scr_emitter_trace";
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) return "scr_stream_trace";
|
||||
return mangleClassTrace(t.className);
|
||||
case "record":
|
||||
return emitter.tracedShapes.has(`record:${t.shapeId}`) ? mangleRecordTrace(t.shapeId) : null;
|
||||
// Cycle-capable when either the key or value type is (the same
|
||||
// rule as the constructor and the tracing fixed point).
|
||||
case "map":
|
||||
return emitter.traceAdapterC(t.key) !== null || emitter.traceAdapterC(t.value) !== null ? "scr_map_trace_v" : null;
|
||||
case "set":
|
||||
return emitter.traceAdapterC(t.elem) !== null ? "scr_map_trace_v" : null;
|
||||
// Arrays mirror maps: cycle-capable exactly when the ELEMENT type is
|
||||
// (a record/object/union element — or a cycle-capable inner array —
|
||||
// can point back at the array holding it). Such arrays allocate with
|
||||
// the collector header (scr_arr_new_ref with a trace); scalar/
|
||||
// string/bytes-element arrays stay lean.
|
||||
case "array":
|
||||
return emitter.traceAdapterC(t.elem) !== null ? "scr_arr_trace_v" : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** `&<trace>` or `NULL` — the trace argument at a container call site. */
|
||||
export function traceArgC(emitter: CEmitter, t: IrType): string {
|
||||
const sym = emitter.traceAdapterC(t);
|
||||
return sym ? `&${sym}` : "NULL";
|
||||
}
|
||||
|
||||
/** Array construction expression for one element type. Ref elements
|
||||
* (records, class instances, unions — and cycle-capable inner arrays,
|
||||
* whose SCR_ELEM_ARR spelling would hide them from the outer array's
|
||||
* trace) construct through scr_arr_new_ref, which stores the element
|
||||
* type's RC entry points once per array (the map-value technique) and
|
||||
* allocates with the collector header exactly when the element type
|
||||
* carries one (trace non-NULL). Every other element kind keeps the
|
||||
* historic scr_arr_new call. */
|
||||
export function arrNewC(emitter: CEmitter, elem: IrType, capExpr: string | number): string {
|
||||
const useRef =
|
||||
elem.kind === "record" || elem.kind === "object" || elem.kind === "union" ||
|
||||
// Collection values retain identity and trace their typed keys/values.
|
||||
elem.kind === "map" || elem.kind === "set" ||
|
||||
// Promise elements (Promise.all's food): refcounted, cycle-headered
|
||||
// — the `_v` adapters and scr_promise_trace_v ride the same REF
|
||||
// machinery as record/object/union elements.
|
||||
elem.kind === "promise" ||
|
||||
elem.kind === "dyn" || // native collection seeds/drains: scr_dyn_* adapters
|
||||
elem.kind === "jsval" || // island handles: scr_jsval_* adapters, no trace
|
||||
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
|
||||
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
|
||||
elem.kind === "netServer" || // server handles: scr_net_server_* adapters, no trace
|
||||
elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace
|
||||
elem.kind === "bigint" || // immutable numeric values: scr_bigint_* adapters
|
||||
elem.kind === "classval" || // local class objects own traced capture boxes
|
||||
// Closures: scr_closure_* adapters + scr_closure_trace_v (always
|
||||
// cycle-headered — captures can reach back through boxes).
|
||||
elem.kind === "func" ||
|
||||
(elem.kind === "array" && emitter.traceAdapterC(elem) !== null);
|
||||
if (!useRef) return `scr_arr_new(${elemKindC(elem)}, ${capExpr})`;
|
||||
const v = vAdapters(elem);
|
||||
return `scr_arr_new_ref(&${v.retain}, &${v.release}, ${emitter.traceArgC(elem)}, ${capExpr})`;
|
||||
}
|
||||
|
||||
/** Box construction expression — object/record/union/promise boxes carry
|
||||
* their RC entry points (and the payload's trace) as function pointers
|
||||
* (the SCR_BOX_OBJ mechanism: the runtime can't know per-shape layouts). */
|
||||
export function boxNewC(emitter: CEmitter, t: IrType): string {
|
||||
// A captured local can be typed by a class the module never collected
|
||||
// (a runtime-fenced JS class — e.g. one declared inside a block —
|
||||
// whose declaration and every use compile to runtime traps): no
|
||||
// instance can ever exist, so the box is an inert placeholder — its
|
||||
// RC adapters were never emitted and must not be referenced.
|
||||
if (t.kind === "object" && !emitter.classMeta.has(t.className)) {
|
||||
return `scr_box_new(SCR_BOX_F64) /* ${t.className}: uncollected class, all uses trap */`;
|
||||
}
|
||||
if (
|
||||
t.kind === "object" || t.kind === "record" || t.kind === "union" ||
|
||||
t.kind === "classval" ||
|
||||
t.kind === "map" || t.kind === "set" || t.kind === "promise" ||
|
||||
t.kind === "generator" ||
|
||||
t.kind === "regex" || t.kind === "url" || t.kind === "searchParams" ||
|
||||
t.kind === "symbol" || t.kind === "bigint" || t.kind === "stats" ||
|
||||
t.kind === "fileHandle" ||
|
||||
t.kind === "spawnRes" || t.kind === "child" || t.kind === "bytes" ||
|
||||
t.kind === "netServer" || t.kind === "netSocket" ||
|
||||
t.kind === "http2Session" || t.kind === "http2Stream" ||
|
||||
t.kind === "dgramSocket" || t.kind === "testCtx" ||
|
||||
t.kind === "httpReq" || t.kind === "httpRes" ||
|
||||
t.kind === "httpClientReq" || t.kind === "secureCtx" ||
|
||||
t.kind === "cryptoHash" || t.kind === "cryptoHmac" ||
|
||||
t.kind === "fsWatcher" || t.kind === "childStream" || t.kind === "childWriter" ||
|
||||
// Island handles: the box carries scr_jsval_retain_v/release_v and
|
||||
// no trace — the same stance as jsval array elements.
|
||||
t.kind === "jsval" ||
|
||||
// Checked values trace native objects, arrays and captured closures.
|
||||
t.kind === "dyn" ||
|
||||
// A CYCLE-CAPABLE array must ride the obj-box so the box's trace
|
||||
// reaches it (SCR_BOX_ARR payloads are never traced); acyclic arrays
|
||||
// keep the historic plain-kind box below.
|
||||
(t.kind === "array" && emitter.traceAdapterC(t) !== null)
|
||||
) {
|
||||
const v = vAdapters(t);
|
||||
return `scr_box_new_obj(&${v.retain}, &${v.release}, ${emitter.traceArgC(t)})`;
|
||||
}
|
||||
return `scr_box_new(${boxKindC(t)})`;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { RUNTIME_EMITTER_CLASS } from "../../ir/ir.js";
|
||||
import { cCommentText, releaseCallC, retainCallC, vAdapters } from "./types.js";
|
||||
|
||||
describe("C comment text", () => {
|
||||
test("preserves ordinary text and neutralizes delimiters and controls", () => {
|
||||
expect(cCommentText('quote" slash\\ unicode-é-😀')).toBe('quote" slash\\ unicode-é-😀');
|
||||
expect(cCommentText("open/*close*/overlap/*/block/**/end")).toBe(
|
||||
"open/ *close* /overlap/ * /block/ ** /end",
|
||||
);
|
||||
expect(cCommentText("nul\0line\nunit\u001fdel\u007fnext\u0085sep\u2028")).toBe(
|
||||
"nul\\u0000line\\u000aunit\\u001fdel\\u007fnext\\u0085sep\\u2028",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime RC symbols", () => {
|
||||
test("derives typed and adapter symbols from the shared stems", () => {
|
||||
expect(retainCallC({ kind: "child" }, "x")).toBe("scr_child_retain(x)");
|
||||
expect(releaseCallC({ kind: "netSocket" }, "x")).toBe("scr_net_sock_release(x)");
|
||||
expect(vAdapters({ kind: "http2Session" })).toEqual({
|
||||
retain: "scr_http2_session_retain_v",
|
||||
release: "scr_http2_session_release_v",
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps runtime and emitted object families distinct", () => {
|
||||
expect(vAdapters({ kind: "object", className: RUNTIME_EMITTER_CLASS })).toEqual({
|
||||
retain: "scr_emitter_retain_v",
|
||||
release: "scr_emitter_release_v",
|
||||
});
|
||||
expect(retainCallC({ kind: "object", className: "Widget" }, "x")).toBe("sc_retain_Widget(x)");
|
||||
expect(vAdapters({ kind: "record", shapeId: "r0" })).toEqual({
|
||||
retain: "sc_rretain_r0_v",
|
||||
release: "sc_rrelease_r0_v",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,488 +0,0 @@
|
||||
import { commentText, octalByte } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Type-directed dispatch tables of the C emitter: the C spelling of every IR
|
||||
* type and the per-type runtime entry points (retain/release, box kinds,
|
||||
* array element kinds, map key/value kinds), plus C literal spelling. Pure
|
||||
* functions of IrType/values — every emission module leans on these, so they
|
||||
* live in ONE place with no emitter state. */
|
||||
import type { IrBytesElem, IrType } from "../../ir/ir.js";
|
||||
import { isIdentityCollectionKey, POINTER_KINDS, type PointerKind, runtimeRcStem, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES } from "../../ir/ir.js";
|
||||
import {
|
||||
mangleClassRelease,
|
||||
mangleClassRetain,
|
||||
mangleClassStruct,
|
||||
mangleRecordRelease,
|
||||
mangleRecordRetain,
|
||||
mangleRecordStruct,
|
||||
} from "../mangle.js";
|
||||
|
||||
type BoxNewPointerType = Extract<IrType, {
|
||||
kind: Exclude<PointerKind, "string" | "array" | "func" | "dyn" | "jsval" | "caught" | "promise" | "generator">
|
||||
}>;
|
||||
type RejectedArrayPointerType = Extract<IrType, {
|
||||
kind: Exclude<PointerKind, "string" | "bigint" | "array" | "map" | "set" | "bytes" | "record" | "object" | "union" | "jsval" | "child" | "netServer" | "symbol" | "classval" | "func">
|
||||
}>;
|
||||
|
||||
export function cType(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
return "double";
|
||||
case "bool":
|
||||
return "bool";
|
||||
case "string":
|
||||
return "ScrStr *";
|
||||
case "moduleNs":
|
||||
return "ScrStr *";
|
||||
case "bigint":
|
||||
return "ScrBigInt *";
|
||||
case "array":
|
||||
return "ScrArr *";
|
||||
case "map":
|
||||
case "set":
|
||||
// Sets ARE the map runtime with the value slot unused (SCR_MAP_VAL_F64
|
||||
// storing a constant 0) — one struct, one RC family, one allocator.
|
||||
return "ScrMap *";
|
||||
case "regex":
|
||||
return "ScrRegex *";
|
||||
case "bytes":
|
||||
// One struct for every element kind (the runtime dispatches on the
|
||||
// stored elem tag) — exactly the ScrArr pattern.
|
||||
return "ScrBytes *";
|
||||
case "url":
|
||||
return "ScrUrl *";
|
||||
case "searchParams":
|
||||
return "ScrSearchParams *";
|
||||
case "symbol":
|
||||
return "ScrSym *";
|
||||
case "stats":
|
||||
return "ScrStats *";
|
||||
case "fileHandle":
|
||||
return "ScrFileHandle *";
|
||||
case "spawnRes":
|
||||
return "ScrSpawnRes *";
|
||||
case "child":
|
||||
return "ScrChild *";
|
||||
case "netServer":
|
||||
return "ScrNetServer *";
|
||||
case "netSocket":
|
||||
return "ScrNetSocket *";
|
||||
case "http2Session":
|
||||
return "ScrH2Session *";
|
||||
case "http2Stream":
|
||||
return "ScrH2Stream *";
|
||||
case "dgramSocket":
|
||||
return "ScrDgramSocket *";
|
||||
case "testCtx":
|
||||
return "ScrTestCtx *";
|
||||
case "httpReq":
|
||||
return "ScrHttpReq *";
|
||||
case "httpRes":
|
||||
return "ScrHttpRes *";
|
||||
case "httpClientReq":
|
||||
return "ScrHttpClientReq *";
|
||||
case "secureCtx":
|
||||
return "ScrSecureCtx *";
|
||||
case "cryptoHash":
|
||||
case "cryptoHmac":
|
||||
return "ScrCryptoHash *";
|
||||
case "fsWatcher":
|
||||
return "ScrWatcher *";
|
||||
case "childStream":
|
||||
return "ScrChildStream *";
|
||||
case "childWriter":
|
||||
return "ScrChildWriter *";
|
||||
case "procStream":
|
||||
// A SCALAR kind: the stream value IS its fd (1 = stdout, 2 =
|
||||
// stderr) — no heap, no refcount.
|
||||
return "double";
|
||||
case "func":
|
||||
return "ScrClosure *";
|
||||
case "classval":
|
||||
// ONE struct type for every class (the fields are class-independent).
|
||||
return "ScrClassObj *";
|
||||
case "object":
|
||||
// Runtime-provided error classes use the ScrError prefix. DOMException
|
||||
// has additional runtime slots; user subclasses embed the prefix.
|
||||
if (RUNTIME_ERROR_CLASSES.has(t.className)) return "ScrError *";
|
||||
// The runtime emitter class shares the runtime's ScrEmitter struct
|
||||
// (user subclasses embed its prefix in their emitted structs).
|
||||
if (t.className === RUNTIME_EMITTER_CLASS) return "ScrEmitter *";
|
||||
// The five runtime stream classes share ONE runtime struct (the
|
||||
// emitter prefix plus the stream-state pointer) — upcasts among
|
||||
// them and to ScrEmitter are pointer reinterprets.
|
||||
if (RUNTIME_STREAM_CLASSES.has(t.className)) return "ScrStream *";
|
||||
return `${mangleClassStruct(t.className)} *`;
|
||||
case "record":
|
||||
return `${mangleRecordStruct(t.shapeId)} *`;
|
||||
case "union":
|
||||
return "ScrUnion *";
|
||||
case "dyn":
|
||||
return "ScrDyn *";
|
||||
case "jsval":
|
||||
return "ScrJsval *";
|
||||
case "caught":
|
||||
return "ScrCaught *";
|
||||
case "promise":
|
||||
return "ScrPromise *";
|
||||
case "generator":
|
||||
return "ScrGen *";
|
||||
case "void":
|
||||
return "void";
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
// Unit kinds have no C value form: they exist only as union arms
|
||||
// (the box carries the tag and nothing else) — a unit type asked to
|
||||
// declare a C value is an emitter bug.
|
||||
throw new InternalCompilerError(`emitter bug: ${t.kind} has no C value form`);
|
||||
default: {
|
||||
const _exhaustive: never = t;
|
||||
void _exhaustive;
|
||||
throw new InternalCompilerError("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The retain call (+1, returns the value) for one refcounted type. */
|
||||
export function retainCallC(type: IrType, expr: string): string {
|
||||
const stem = runtimeRcStem(type);
|
||||
if (stem !== null) return `${stem}_retain(${expr})`;
|
||||
switch (type.kind) {
|
||||
case "object":
|
||||
return `${mangleClassRetain(type.className)}(${expr})`;
|
||||
case "record":
|
||||
return `${mangleRecordRetain(type.shapeId)}(${expr})`;
|
||||
default:
|
||||
throw new InternalCompilerError(`emitter bug: retain of non-refcounted type ${type.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The release call for one owned refcounted value (all NULL-tolerant). */
|
||||
export function releaseCallC(type: IrType, expr: string): string {
|
||||
const stem = runtimeRcStem(type);
|
||||
if (stem !== null) return `${stem}_release(${expr})`;
|
||||
switch (type.kind) {
|
||||
case "object":
|
||||
return `${mangleClassRelease(type.className)}(${expr})`;
|
||||
case "record":
|
||||
return `${mangleRecordRelease(type.shapeId)}(${expr})`;
|
||||
default:
|
||||
throw new InternalCompilerError(`emitter bug: release of non-refcounted type ${type.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime's box-kind tag for a boxed (captured) variable's type. */
|
||||
export function boxKindC(t: IrType): string {
|
||||
if (POINTER_KINDS.has(t.kind) &&
|
||||
t.kind !== "string" && t.kind !== "array" && t.kind !== "func" &&
|
||||
t.kind !== "dyn" && t.kind !== "jsval" && t.kind !== "caught" &&
|
||||
t.kind !== "promise" && t.kind !== "generator") {
|
||||
throw new InternalCompilerError(`emitter bug: ${t.kind} boxes go through boxNewC, not boxKindC`);
|
||||
}
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
return "SCR_BOX_F64";
|
||||
case "bool":
|
||||
return "SCR_BOX_BOOL";
|
||||
case "string":
|
||||
return "SCR_BOX_STR";
|
||||
case "array":
|
||||
return "SCR_BOX_ARR";
|
||||
case "func":
|
||||
return "SCR_BOX_FUNC";
|
||||
case "procStream":
|
||||
// Scalar (the fd double) — the f64 box carries it.
|
||||
return "SCR_BOX_F64";
|
||||
case "dyn":
|
||||
// dyn never rides capture boxes (frontend rejects dyn captures).
|
||||
throw new InternalCompilerError("emitter bug: box of dyn");
|
||||
case "jsval":
|
||||
throw new InternalCompilerError("emitter bug: jsval boxes go through boxNewC, not boxKindC");
|
||||
case "promise":
|
||||
// promises ride obj-boxes (boxNewC), never plain kind boxes
|
||||
throw new InternalCompilerError("emitter bug: promise boxes go through boxNewC");
|
||||
case "generator":
|
||||
// generators ride obj-boxes too (boxNewC — vAdapters carries the
|
||||
// ScrGen RC entry points; no trace, like child).
|
||||
throw new InternalCompilerError("emitter bug: generator boxes go through boxNewC");
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
case "caught":
|
||||
// unit kinds never stand alone (and catch bindings never box), so
|
||||
// nothing to box
|
||||
throw new InternalCompilerError(`emitter bug: box of ${t.kind}`);
|
||||
case "void":
|
||||
throw new InternalCompilerError("emitter bug: box of void");
|
||||
default: {
|
||||
const _exhaustive: never = t as Exclude<typeof t, BoxNewPointerType>;
|
||||
void _exhaustive;
|
||||
throw new InternalCompilerError("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime `_v` (void*-signature) RC entry points for one refcounted
|
||||
* type — the currency of every generic container that must retain/release
|
||||
* payloads whose concrete struct it cannot know: union values, capture
|
||||
* boxes, promises, and the exception cell. Classes and records use their
|
||||
* emitted per-shape adapters; everything else has runtime-provided ones.
|
||||
* Bare symbol names — call sites prefix `&` where a fn ptr is passed. */
|
||||
export function vAdapters(t: IrType): { retain: string; release: string } {
|
||||
const stem = runtimeRcStem(t);
|
||||
if (stem !== null && t.kind !== "caught") {
|
||||
return { retain: `${stem}_retain_v`, release: `${stem}_release_v` };
|
||||
}
|
||||
switch (t.kind) {
|
||||
case "object":
|
||||
return { retain: `${mangleClassRetain(t.className)}_v`, release: `${mangleClassRelease(t.className)}_v` };
|
||||
case "record":
|
||||
return { retain: `${mangleRecordRetain(t.shapeId)}_v`, release: `${mangleRecordRelease(t.shapeId)}_v` };
|
||||
default:
|
||||
throw new InternalCompilerError(`emitter bug: no RC adapters for ${t.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Box accessor suffix: scalars stored unboxed, ref kinds as pointers. */
|
||||
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
// procStream is a scalar (the stream's fd double — boxKindC agrees with
|
||||
// SCR_BOX_F64), so its captures ride the f64 slot like any number.
|
||||
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/** C function-pointer cast for calling through a closure: the callee
|
||||
* receives its own ScrClosure first, then the declared params. */
|
||||
export function cFnPtrCast(ft: IrType & { kind: "func" }): string {
|
||||
const params = ["ScrClosure *", ...ft.params.map((p) => cType(p).trim())].join(", ");
|
||||
return `(${cType(ft.ret).trim()} (*)(${params}))`;
|
||||
}
|
||||
|
||||
/** The runtime's element-kind tag for an array's element type. Record/
|
||||
* object/union elements are SCR_ELEM_REF (void* slots + per-array RC entry
|
||||
* points); array elements answer SCR_ELEM_ARR here, but a CYCLE-CAPABLE
|
||||
* inner array rides SCR_ELEM_REF instead so the outer array's trace can
|
||||
* reach it — that answer needs emitter state, so construction goes through
|
||||
* CEmitter.arrNewC, which overrides this for traced-array elements. */
|
||||
export function elemKindC(elem: IrType): string {
|
||||
if (POINTER_KINDS.has(elem.kind) &&
|
||||
elem.kind !== "string" && elem.kind !== "array" && elem.kind !== "map" && elem.kind !== "set" && elem.kind !== "bytes" &&
|
||||
elem.kind !== "record" && elem.kind !== "object" && elem.kind !== "union" &&
|
||||
elem.kind !== "jsval" && elem.kind !== "child" && elem.kind !== "netServer" &&
|
||||
elem.kind !== "symbol" && elem.kind !== "bigint" && elem.kind !== "classval" && elem.kind !== "func" && elem.kind !== "dyn") {
|
||||
throw new InternalCompilerError(`emitter bug: array of ${elem.kind} (frontend rejects these)`);
|
||||
}
|
||||
switch (elem.kind) {
|
||||
case "f64":
|
||||
return "SCR_ELEM_F64";
|
||||
case "bool":
|
||||
return "SCR_ELEM_BOOL";
|
||||
case "string":
|
||||
return "SCR_ELEM_STR";
|
||||
case "array":
|
||||
return "SCR_ELEM_ARR";
|
||||
case "bytes":
|
||||
return "SCR_ELEM_BYTES";
|
||||
case "record":
|
||||
case "object":
|
||||
case "union":
|
||||
case "map":
|
||||
case "set":
|
||||
// Island handles are ordinary refcounted pointers (scr_jsval_retain/
|
||||
// release adapters) — `any[]` under --dynamic is a native array of
|
||||
// handles, one element per island value.
|
||||
case "jsval":
|
||||
case "dyn":
|
||||
// Spawned child handles (ChildProcess[] — the running-apps list):
|
||||
// ordinary refcounted pointers, no trace (they drop their closures at
|
||||
// reap, so never part of a cycle).
|
||||
case "child":
|
||||
// Server handles (ProxyServer[] — the [...set] drain of the auxiliary
|
||||
// registries): same refcounted-pointer story; listeners drop at close,
|
||||
// so a handle-in-array cycle is temporary like child's.
|
||||
case "netServer":
|
||||
// Symbols (symbol[] — heterogeneous sentinel lists): refcounted
|
||||
// identity pointers holding only strings — no trace, no cycles ever.
|
||||
case "symbol":
|
||||
case "bigint":
|
||||
// Class objects ((typeof Shape)[] — the registry idiom): immortal
|
||||
// statics behind no-op RC adapters — no trace, no cycles ever;
|
||||
// indexOf/includes/=== are the REF kind's pointer identity, exactly
|
||||
// JS class identity.
|
||||
case "classval":
|
||||
return "SCR_ELEM_REF";
|
||||
// Closures: refcounted, cycle-headered (captures can reach back
|
||||
// through boxes), scr_closure_* `_v` adapters + scr_closure_trace_v.
|
||||
// Identity semantics (indexOf/includes/===) are the REF kind's
|
||||
// pointer identity — exactly JS function identity.
|
||||
case "func":
|
||||
return "SCR_ELEM_REF";
|
||||
case "date":
|
||||
case "procStream":
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
throw new InternalCompilerError(`emitter bug: array of ${elem.kind} (frontend rejects these)`);
|
||||
case "void":
|
||||
throw new InternalCompilerError("emitter bug: array of void");
|
||||
default: {
|
||||
const _exhaustive: never = elem as Exclude<typeof elem, RejectedArrayPointerType>;
|
||||
void _exhaustive;
|
||||
throw new InternalCompilerError("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The runtime's element-kind tag for a bytes (typed array) type. */
|
||||
const BYTES_ELEM_KIND_C: Record<IrBytesElem, string> = {
|
||||
u8: "SCR_BYTES_U8",
|
||||
u8c: "SCR_BYTES_U8C",
|
||||
i8: "SCR_BYTES_I8",
|
||||
u16: "SCR_BYTES_U16",
|
||||
i16: "SCR_BYTES_I16",
|
||||
u32: "SCR_BYTES_U32",
|
||||
i32: "SCR_BYTES_I32",
|
||||
f32: "SCR_BYTES_F32",
|
||||
f64: "SCR_BYTES_F64",
|
||||
};
|
||||
|
||||
export function bytesElemKindC(elem: IrBytesElem): string {
|
||||
return BYTES_ELEM_KIND_C[elem];
|
||||
}
|
||||
|
||||
/** The runtime's ScrBytesNumKind tag + littleEndian flag per readNum/
|
||||
* writeNum kind token (the strLit args[0] the frontend mints). The
|
||||
* variable-width family's tokens map to sign + endian flags instead. */
|
||||
export const BYTES_NUM_KIND_C: Record<string, { kind: string; le: boolean } | undefined> = {
|
||||
u8: { kind: "SCR_BN_U8", le: false },
|
||||
i8: { kind: "SCR_BN_I8", le: false },
|
||||
u16be: { kind: "SCR_BN_U16", le: false },
|
||||
u16le: { kind: "SCR_BN_U16", le: true },
|
||||
i16be: { kind: "SCR_BN_I16", le: false },
|
||||
i16le: { kind: "SCR_BN_I16", le: true },
|
||||
u32be: { kind: "SCR_BN_U32", le: false },
|
||||
u32le: { kind: "SCR_BN_U32", le: true },
|
||||
i32be: { kind: "SCR_BN_I32", le: false },
|
||||
i32le: { kind: "SCR_BN_I32", le: true },
|
||||
f32be: { kind: "SCR_BN_F32", le: false },
|
||||
f32le: { kind: "SCR_BN_F32", le: true },
|
||||
f64be: { kind: "SCR_BN_F64", le: false },
|
||||
f64le: { kind: "SCR_BN_F64", le: true },
|
||||
};
|
||||
|
||||
/** The variable-width (read/writeUIntLE-style) kind tokens: sign + endian. */
|
||||
export const BYTES_NUM_VAR_C: Record<string, { sign: boolean; le: boolean } | undefined> = {
|
||||
ube: { sign: false, le: false },
|
||||
ule: { sign: false, le: true },
|
||||
ibe: { sign: true, le: false },
|
||||
ile: { sign: true, le: true },
|
||||
};
|
||||
|
||||
/** The runtime's ScrDataViewGet tag per dvGet* bytesIntrinsic method. */
|
||||
export const DV_GET_KIND_C: Record<string, string> = {
|
||||
dvGetUint8: "SCR_DV_U8",
|
||||
dvGetInt8: "SCR_DV_I8",
|
||||
dvGetUint16: "SCR_DV_U16",
|
||||
dvGetInt16: "SCR_DV_I16",
|
||||
dvGetUint32: "SCR_DV_U32",
|
||||
dvGetInt32: "SCR_DV_I32",
|
||||
dvGetFloat32: "SCR_DV_F32",
|
||||
dvGetFloat64: "SCR_DV_F64",
|
||||
dvGetBigUint64Number: "SCR_DV_BIGU64",
|
||||
dvGetBigInt64Number: "SCR_DV_BIGI64",
|
||||
};
|
||||
|
||||
/** The runtime's ScrDataViewGet tag per dvSet* bytesIntrinsic method (the
|
||||
* setters reuse the getter kinds; no BIG setters exist). */
|
||||
export const DV_SET_KIND_C: Record<string, string> = {
|
||||
dvSetUint8: "SCR_DV_U8",
|
||||
dvSetInt8: "SCR_DV_I8",
|
||||
dvSetUint16: "SCR_DV_U16",
|
||||
dvSetInt16: "SCR_DV_I16",
|
||||
dvSetUint32: "SCR_DV_U32",
|
||||
dvSetInt32: "SCR_DV_I32",
|
||||
dvSetFloat32: "SCR_DV_F32",
|
||||
dvSetFloat64: "SCR_DV_F64",
|
||||
};
|
||||
|
||||
/** Runtime accessor suffix for an element type: arrays store f64 and bool
|
||||
* unboxed and everything refcounted as a pointer (`_ref`). */
|
||||
export function elemAccess(elem: IrType): "f64" | "bool" | "ref" {
|
||||
return elem.kind === "f64" ? "f64" : elem.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/** Runtime suffix for a map's KEY kind (f64 with SameValueZero, or string
|
||||
* content) — the first suffix of the scr_map_* two-suffix family. */
|
||||
export function mapKeyAccess(key: IrType): "f64" | "str" | "ref" {
|
||||
if (key.kind === "f64") return "f64";
|
||||
if (key.kind === "string") return "str";
|
||||
// Identity references share a pointer ABI; union keys select a separate
|
||||
// hash/equality kind below so their wrapper is not treated as the key.
|
||||
if (isIdentityCollectionKey(key) || key.kind === "union" || key.kind === "dyn") return "ref";
|
||||
throw new InternalCompilerError(`emitter bug: map key of ${key.kind} (frontend rejects these)`);
|
||||
}
|
||||
|
||||
/** The runtime's key-kind/value-kind tags for scr_map_new. */
|
||||
export function mapKeyKindC(key: IrType): string {
|
||||
if (key.kind === "dyn") return "SCR_MAP_KEY_DYN";
|
||||
const acc = mapKeyAccess(key);
|
||||
return key.kind === "union" ? "SCR_MAP_KEY_UNION_REF" : acc === "str" ? "SCR_MAP_KEY_STR" : acc === "ref" ? "SCR_MAP_KEY_REF" : "SCR_MAP_KEY_F64";
|
||||
}
|
||||
|
||||
export function mapValKindC(value: IrType): string {
|
||||
return value.kind === "f64"
|
||||
? "SCR_MAP_VAL_F64"
|
||||
: value.kind === "bool"
|
||||
? "SCR_MAP_VAL_BOOL"
|
||||
: "SCR_MAP_VAL_REF";
|
||||
}
|
||||
|
||||
/** User-controlled text embedded inside a C block comment. Preserve
|
||||
* ordinary output byte-for-byte, but split comment delimiters and encode
|
||||
* source-control characters so the text cannot alter the translation unit. */
|
||||
export function cCommentText(text: string): string {
|
||||
return commentText(text).replace(/\*\//g, "* /").replace(/\/\*/g, "/ *");
|
||||
}
|
||||
|
||||
/** UTF-8 bytes as an unambiguous C string literal (octal escapes are always
|
||||
* three digits, so a following digit can never extend them — unlike \xHH). */
|
||||
export function cStringLiteral(bytes: Buffer): string {
|
||||
let out = '"';
|
||||
for (const b of bytes) {
|
||||
if (b === 0x22) out += '\\"';
|
||||
else if (b === 0x5c) out += "\\\\";
|
||||
// '?' escapes to defuse TRIGRAPHS: under -std=c11 the preprocessor
|
||||
// rewrites `??=` (and the other eight `??x` sequences) INSIDE string
|
||||
// literals — an embedded JS `wasmBinaryFile ??= f()` would reach the
|
||||
// engine as `wasmBinaryFile #` and die as a SyntaxError. `\?` is
|
||||
// standard C, exactly for this.
|
||||
else if (b === 0x3f) out += "\\?";
|
||||
else if (b >= 0x20 && b < 0x7f) out += String.fromCharCode(b);
|
||||
else out += "\\" + octalByte(b);
|
||||
}
|
||||
return out + '"';
|
||||
}
|
||||
|
||||
/** JS shortest-roundtrip decimal re-parses to the identical double in C
|
||||
* (strtod is correctly rounded), so String(value) is a faithful C literal. */
|
||||
export function cNumberLiteral(value: number): string {
|
||||
// ±Infinity and NaN numLits are real (the globals `Infinity`/`NaN`); C
|
||||
// spells them with math.h's INFINITY/NAN macros (the emitted unit always
|
||||
// includes math.h).
|
||||
if (value === Infinity) return "INFINITY";
|
||||
if (value === -Infinity) return "-INFINITY";
|
||||
if (Number.isNaN(value)) return "NAN";
|
||||
if (Object.is(value, -0)) return "-0.0"; // String(-0) is "0", which would lose the sign
|
||||
const text = String(value);
|
||||
// Integral shortest-roundtrip text ("118059162071741140000") would be a
|
||||
// C INTEGER literal — invalid beyond unsigned long long's range, and in
|
||||
// [2^63, 2^64) clang's unsigned-interpretation extension would let a
|
||||
// NEGATED literal wrap modulo 2^64, a silent wrong value; the ".0" keeps
|
||||
// every numLit a double literal (decimal parsing is correctly rounded
|
||||
// either way).
|
||||
return /[.eE]/.test(text) ? text : text + ".0";
|
||||
}
|
||||
|
||||
/** `<type> <name>` with pointer types spaced C-style (`ScrStr *x`). */
|
||||
export function cDecl(type: IrType, name: string): string {
|
||||
const t = cType(type);
|
||||
return t.endsWith("*") ? t + name : `${t} ${name}`;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN, F64, STRING, VOID, RUNTIME_EMITTER_CLASS, arrayOf, funcOf, mapOf, setOf, type IrModule, type IrRecordShape, type IrType } from "../ir/ir.js";
|
||||
import { computeTraced } from "./cycle-analysis.js";
|
||||
import { CEmitter } from "./c/c-emitter.js";
|
||||
import { computeTraced as llvmTraced } from "./llvm/shapes.js";
|
||||
|
||||
const loc = { file: "cycles.ts", start: 0, end: 1 };
|
||||
@@ -17,9 +16,6 @@ function check(mod: IrModule, shapes: string[], unions: string[] = []): void {
|
||||
expect([...actual.shapes].sort()).toEqual(shapes.sort());
|
||||
expect([...actual.unions].sort()).toEqual(unions.sort());
|
||||
expect(llvmTraced(mod)).toEqual(actual);
|
||||
const c = new CEmitter(mod);
|
||||
expect(c.tracedShapes).toEqual(actual.shapes);
|
||||
expect(c.tracedUnions).toEqual(actual.unions);
|
||||
expect(mod).toEqual(before);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IrModule, IrType } from "../ir/ir.js";
|
||||
import { funcOf, mapOf, RUNTIME_EMITTER_CLASS, STRING, VOID } from "../ir/ir.js";
|
||||
|
||||
/** Cycle capability shared by both native backends.
|
||||
/** Cycle capability for native code generation.
|
||||
* Greatest fixpoint over shapes and unions: start optimistic (everything
|
||||
* cycle-capable), repeatedly drop shapes with no cycle-capable field and
|
||||
* unions with no cycle-capable arm until stable. Closures, checked values and promises
|
||||
|
||||
@@ -1,51 +1,44 @@
|
||||
/** Runtime requirements derived from the lowered program, shared by compiler hosts. */
|
||||
import {
|
||||
moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesAssert, moduleUsesBigInt,
|
||||
moduleUsesCopying, moduleUsesDc, moduleUsesDgram, moduleUsesDynAsync, moduleUsesDynInvoke,
|
||||
moduleUsesEmitter, moduleUsesFetch, moduleUsesFileHandle, moduleUsesFsWatch, moduleUsesHttp2,
|
||||
moduleUsesHttpServer, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesNet,
|
||||
moduleUsesNodeTest, moduleUsesParseArgs, moduleUsesProcessEvents, moduleUsesQs, moduleUsesRegex,
|
||||
moduleUsesSearchParams, moduleUsesStream, moduleUsesSymbol, moduleUsesTls, moduleUsesTlsCa,
|
||||
moduleUsesZlib, type IrModule,
|
||||
} from "../ir/ir.js";
|
||||
import { moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleRuntimeFeatures, type IrModule } from "../ir/ir.js";
|
||||
import { hasForeignFfiCallback } from "./ffi-callbacks.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
|
||||
export function executableLinkFeatures(mod: IrModule, dynamic: boolean): NativeLinkFeatures {
|
||||
const features = moduleRuntimeFeatures(mod);
|
||||
return {
|
||||
dynamic,
|
||||
regex: moduleUsesRegex(mod),
|
||||
copying: moduleUsesCopying(mod),
|
||||
textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
|
||||
fileHandle: moduleUsesFileHandle(mod),
|
||||
fetch: moduleUsesFetch(mod),
|
||||
regex: features.regex,
|
||||
copying: features.copying,
|
||||
textDecoderLegacy: features.legacyTextDecoder,
|
||||
fileHandle: features.fileHandle,
|
||||
fetch: features.fetch,
|
||||
netIsland:
|
||||
moduleEmbedsBuiltin(mod, "node:http") ||
|
||||
moduleEmbedsBuiltin(mod, "node:https") ||
|
||||
moduleEmbedsBuiltin(mod, "node:net") ||
|
||||
moduleEmbedsBuiltin(mod, "node:tls"),
|
||||
zlib: moduleUsesZlib(mod) || moduleEmbedsCompressedNpm(mod),
|
||||
assert: moduleUsesAssert(mod),
|
||||
inspect: moduleUsesInspect(mod),
|
||||
dynInvoke: moduleUsesDynInvoke(mod),
|
||||
dc: moduleUsesDc(mod),
|
||||
dynAsync: moduleUsesDynAsync(mod),
|
||||
events: moduleUsesProcessEvents(mod),
|
||||
emitter: moduleUsesEmitter(mod),
|
||||
symbol: moduleUsesSymbol(mod),
|
||||
bigint: moduleUsesBigInt(mod),
|
||||
searchParams: moduleUsesSearchParams(mod),
|
||||
qs: moduleUsesQs(mod),
|
||||
parseArgs: moduleUsesParseArgs(mod),
|
||||
stream: moduleUsesStream(mod),
|
||||
net: moduleUsesNet(mod),
|
||||
http: moduleUsesHttpServer(mod),
|
||||
http2: moduleUsesHttp2(mod),
|
||||
dgram: moduleUsesDgram(mod),
|
||||
watch: moduleUsesFsWatch(mod),
|
||||
zlib: features.zlib || moduleEmbedsCompressedNpm(mod),
|
||||
assert: features.assert,
|
||||
inspect: features.inspect,
|
||||
dynInvoke: features.dynInvoke,
|
||||
dc: features.dc,
|
||||
dynAsync: features.dynAsync,
|
||||
events: features.processEvents,
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
bigint: features.bigint,
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
parseArgs: features.parseArgs,
|
||||
stream: features.stream,
|
||||
net: features.net,
|
||||
http: features.http,
|
||||
http2: features.http2,
|
||||
dgram: features.dgram,
|
||||
watch: features.fsWatch,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: moduleUsesNodeTest(mod),
|
||||
tls: moduleUsesTls(mod),
|
||||
tlsCa: moduleUsesTlsCa(mod),
|
||||
nodeTest: features.nodeTest,
|
||||
tls: features.tls,
|
||||
tlsCa: features.tlsCa,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
/**
|
||||
* The explicitly external C toolchain path.
|
||||
*
|
||||
* This module is deliberately narrow at the executable boundary: normal
|
||||
* macOS arm64 LLVM builds emit a program object through the bundled helper
|
||||
* and hand it to `linker.ts`. Generated C, `--from-c`, sanitizer builds,
|
||||
* cross targets, and the temporary LLVM fallback continue to use this path.
|
||||
* Keeping that distinction here prevents a platform linker driver from
|
||||
* accidentally becoming the abstraction that compiles scriptc programs.
|
||||
*/
|
||||
/** External LLVM/C compilation for instrumented runtime development and
|
||||
* caller-supplied native sources. Production program objects use the
|
||||
* bundled LLVM helper and precompiled runtime packs. */
|
||||
import {
|
||||
compileC,
|
||||
compileLibArchive,
|
||||
@@ -50,52 +43,12 @@ export {
|
||||
} from "./native-toolchain.js";
|
||||
|
||||
/** Compile a caller-provided C or LLVM source file through an external C
|
||||
* toolchain. The CLI uses this only for its explicit `--from-c` escape
|
||||
* hatch; generated program C uses the same implementation only on the
|
||||
* documented legacy/fallback routes. */
|
||||
* toolchain. Runtime development and native embedding tests also use this utility. */
|
||||
export async function compileExternalC(options: CcOptions): Promise<void> {
|
||||
await compileC(options);
|
||||
}
|
||||
|
||||
/** Build a library archive through the external C toolchain. Library packs
|
||||
* are intentionally out of scope for the first executable-linking split. */
|
||||
/** Build an instrumented development library through the external toolchain. */
|
||||
export async function compileExternalCLibrary(options: LibArchiveOptions): Promise<void> {
|
||||
await compileLibArchive(options);
|
||||
}
|
||||
|
||||
/** Internal migration guard used by CI to ensure an LLVM-tier executable did
|
||||
* not regress from helper-object linking to compiling its generated .ll.
|
||||
* Explicit `--backend=c` and `--from-c` remain deliberate developer paths. */
|
||||
export function legacyCExecutablePipelineEnabled(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return env["SCRIPTC_LEGACY_C_PIPELINE"] !== "0";
|
||||
}
|
||||
|
||||
/** `SCRIPTC_CC` selects the external C compiler route during the migration.
|
||||
* An unset value lets every supported LLVM-tier target use its helper plus
|
||||
* runtime pack and reserve `SCRIPTC_LINKER` for the platform-linker driver. */
|
||||
export function legacyCExecutablePathRequested(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const configured = env["SCRIPTC_CC"];
|
||||
return configured !== undefined && configured !== "";
|
||||
}
|
||||
|
||||
export class LegacyCExecutablePipelineDisabledError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
"the legacy generated-C executable pipeline is disabled (SCRIPTC_LEGACY_C_PIPELINE=0); " +
|
||||
"use the supported LLVM helper/runtime-pack route or remove the internal comparison switch",
|
||||
);
|
||||
this.name = "LegacyCExecutablePipelineDisabledError";
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLegacyCExecutablePipelineEnabled(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): void {
|
||||
if (!legacyCExecutablePipelineEnabled(env)) {
|
||||
throw new LegacyCExecutablePipelineDisabledError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface FfiCallbackAdapter {
|
||||
}
|
||||
|
||||
/** Allocate internal callback symbols outside the manifest's external
|
||||
* symbol set. C and LLVM share this table so a valid native symbol can
|
||||
* symbol set. The frontend and backend share this table so a valid native symbol can
|
||||
* never collide with a generated trampoline or raw-callback TLS slot. */
|
||||
export function allocateFfiCallbackAdapters(
|
||||
imports: readonly IrFfiImport[],
|
||||
@@ -109,8 +109,7 @@ export interface FfiRetainedOp<V> {
|
||||
}
|
||||
|
||||
/** Collect a call's retained registrations and releases in manifest order —
|
||||
* the lifecycle-policy walk shared by the C and LLVM emitters, so ordering
|
||||
* fixes apply to both backends at once. */
|
||||
* the runtime and LLVM adapters follow the same lifecycle ordering. */
|
||||
export function collectFfiRetainedOps<V>(
|
||||
entry: IrFfiImport,
|
||||
callbackArgs: ReadonlyMap<string, V>,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { InternalCompilerError } from "../errors.js";
|
||||
export type LibraryEmission = "c" | "llvm";
|
||||
|
||||
const C_LIBRARY_IDENTITY_BEGIN = "/* scriptc-library-identity: begin */";
|
||||
const C_LIBRARY_IDENTITY_END = "/* scriptc-library-identity: end */";
|
||||
const LLVM_LIBRARY_IDENTITY_BEGIN = "; scriptc-library-identity: begin";
|
||||
const LLVM_LIBRARY_IDENTITY_END = "; scriptc-library-identity: end";
|
||||
|
||||
@@ -13,34 +10,17 @@ export interface LibraryIdentityValues {
|
||||
abiVersion: number;
|
||||
}
|
||||
|
||||
function identityMarkers(emission: LibraryEmission): { begin: string; end: string } {
|
||||
return emission === "c"
|
||||
? { begin: C_LIBRARY_IDENTITY_BEGIN, end: C_LIBRARY_IDENTITY_END }
|
||||
: { begin: LLVM_LIBRARY_IDENTITY_BEGIN, end: LLVM_LIBRARY_IDENTITY_END };
|
||||
function identityMarkers(): { begin: string; end: string } {
|
||||
return { begin: LLVM_LIBRARY_IDENTITY_BEGIN, end: LLVM_LIBRARY_IDENTITY_END };
|
||||
}
|
||||
|
||||
export function emitLibraryIdentityLines(
|
||||
emission: LibraryEmission,
|
||||
identity: LibraryIdentityValues,
|
||||
llvmFunctionAttrs = "#0",
|
||||
): string[] {
|
||||
if (!/^[0-9a-f]{16}$/.test(identity.buildId)) {
|
||||
throw new InternalCompilerError("library build id must be exactly 16 lowercase hex digits");
|
||||
}
|
||||
if (emission === "c") {
|
||||
return [
|
||||
C_LIBRARY_IDENTITY_BEGIN,
|
||||
`uint64_t ${identity.buildIdSymbol}(void) {`,
|
||||
` return UINT64_C(0x${identity.buildId});`,
|
||||
`}`,
|
||||
``,
|
||||
`uint32_t ${identity.abiVersionSymbol}(void) {`,
|
||||
` return ${identity.abiVersion}u;`,
|
||||
`}`,
|
||||
``,
|
||||
C_LIBRARY_IDENTITY_END,
|
||||
];
|
||||
}
|
||||
const signedBuildId = BigInt.asIntN(64, BigInt(`0x${identity.buildId}`)).toString();
|
||||
return [
|
||||
LLVM_LIBRARY_IDENTITY_BEGIN,
|
||||
@@ -60,9 +40,8 @@ export function emitLibraryIdentityLines(
|
||||
|
||||
function identityOffsets(
|
||||
source: string,
|
||||
emission: LibraryEmission,
|
||||
): { start: number; end: number } | null {
|
||||
const { begin, end } = identityMarkers(emission);
|
||||
const { begin, end } = identityMarkers();
|
||||
const start = source.indexOf(`${begin}\n`);
|
||||
if (start < 0) return null;
|
||||
if (source.indexOf(begin, start + begin.length) >= 0) {
|
||||
@@ -76,55 +55,21 @@ function identityOffsets(
|
||||
/** Refresh only the volatile identity block in a cached public TU. */
|
||||
export function replaceLibraryIdentity(
|
||||
source: string,
|
||||
emission: LibraryEmission,
|
||||
identity: LibraryIdentityValues,
|
||||
): string {
|
||||
const offsets = identityOffsets(source, emission);
|
||||
const offsets = identityOffsets(source);
|
||||
if (offsets === null) throw new InternalCompilerError("generated public library TU has no identity region");
|
||||
const replacement = emitLibraryIdentityLines(emission, identity).join("\n");
|
||||
const replacement = emitLibraryIdentityLines(identity).join("\n");
|
||||
return source.slice(0, offsets.start) + replacement + source.slice(offsets.end);
|
||||
}
|
||||
|
||||
function escapedRegExp(text: string): string {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function sourceCommentPattern(sourceFile: string): RegExp {
|
||||
// Emission appends these comments at physical line ends. Requiring that
|
||||
// boundary prevents source-looking text inside a generated C string from
|
||||
// being mistaken for an annotation and changing program semantics. An
|
||||
// uninitialized reference declaration appends its own `/* let ... */`
|
||||
// explanation after the location, so admit that one generated suffix too.
|
||||
return new RegExp(
|
||||
` /\\* ${escapedRegExp(sourceFile)}:(\\d+) \\*/` +
|
||||
`(?= /\\* let [^\\r\\n]*; \\*/(?=\\r?\\n|$)|\\r?\\n|$)`,
|
||||
"g",
|
||||
);
|
||||
}
|
||||
|
||||
/** Refresh the source-line annotations retained in a caller-visible C TU. */
|
||||
export function rebaseLibrarySourceComments(
|
||||
source: string,
|
||||
sourceFile: string,
|
||||
rebaseLine: (line: number) => number,
|
||||
): string {
|
||||
return source.replace(sourceCommentPattern(sourceFile), (_match, line: string) =>
|
||||
` /* ${sourceFile}:${rebaseLine(Number(line))} */`);
|
||||
}
|
||||
|
||||
/** Remove C-only debugging annotations from the private native-cache input. */
|
||||
export function stripLibrarySourceComments(source: string, sourceFile: string): string {
|
||||
return source.replace(sourceCommentPattern(sourceFile), "");
|
||||
}
|
||||
|
||||
/** Remove the generated identity region from a complete caller-visible
|
||||
* library TU. Archive assembly compiles this stable projection beside the
|
||||
* small volatile identity C object, while the public TU remains complete. */
|
||||
* small volatile identity object, while the public TU remains complete. */
|
||||
export function stripLibraryIdentity(
|
||||
source: string,
|
||||
emission: LibraryEmission,
|
||||
): string {
|
||||
const offsets = identityOffsets(source, emission);
|
||||
const offsets = identityOffsets(source);
|
||||
if (offsets === null) return source;
|
||||
const endOffset = offsets.end;
|
||||
const suffix = source[endOffset] === "\n" ? endOffset + 1 : endOffset;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
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 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";
|
||||
|
||||
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> {
|
||||
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,
|
||||
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,
|
||||
dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false,
|
||||
};
|
||||
const selection = await loadRuntimePack({
|
||||
target, features, optimization,
|
||||
mode: options.threadInstances ? "library-thread" : "library",
|
||||
});
|
||||
const stage = await stageRuntimePackArtifacts(selection);
|
||||
try {
|
||||
const buildDir = await mkdtemp(join(tmpdir(), "scriptc-library-pack-"));
|
||||
try {
|
||||
const driver: CcDriver = {
|
||||
argv: [target.defaultLinker, ...target.defaultLinkerArgs],
|
||||
target: target.defaultLinker === "clang" && target.platform === "linux" && process.platform === "linux" && target.architecture === process.arch ? null : target.llvmTriple,
|
||||
zigTarget: target.linkerTargetTriple,
|
||||
targetArgs: ["-target", target.linkerTargetTriple], linkArgs: [],
|
||||
};
|
||||
const archiver = isZigDriver(driver) ? [driver.argv[0]!, "ar"] : ["ar"];
|
||||
const stem = basename(options.cPath).replace(/\.ll$/, "");
|
||||
const emit = async (source: string, name: string): Promise<string> => {
|
||||
const outputPath = join(buildDir, name);
|
||||
await emitNativeArtifact({
|
||||
outputPath, llvm: source, sourcePath: options.cPath,
|
||||
outputKind: "obj", optimization: optimization === "dev" ? "0" : "2", target,
|
||||
});
|
||||
return outputPath;
|
||||
};
|
||||
let program: string;
|
||||
const shards = options.programShards;
|
||||
if (shards !== undefined && shards.length > 1 && options.programPublicSymbols !== undefined) {
|
||||
const objects: string[] = [];
|
||||
// Limit active helper processes: large compiler libraries otherwise
|
||||
// multiply LLVM's peak memory by the number of shards.
|
||||
for (let start = 0; start < shards.length; start += 4) {
|
||||
objects.push(...await Promise.all(shards.slice(start, start + 4).map((shard, offset) =>
|
||||
emit(shard.source, `shard-${start + offset}.o`)
|
||||
)));
|
||||
}
|
||||
program = await localizeLibraryObjects(driver, archiver, buildDir, objects, [], options.programPublicSymbols, stem, target.platform);
|
||||
} else {
|
||||
program = await emit(options.programSource ?? await readFile(options.cPath, "utf8"), `${stem}.o`);
|
||||
}
|
||||
const roots = [program];
|
||||
if (options.identityLlvmSource !== undefined) roots.push(await emit(options.identityLlvmSource, `${stem}.identity.o`));
|
||||
const support = selection.runtimeObjects.map((path) => stage.replacements.get(path)!);
|
||||
for (const [index, archive] of selection.archives.entries()) {
|
||||
const input = stage.replacements.get(archive)!;
|
||||
const listing = await run(archiver[0]!, [...archiver.slice(1), "t", input]);
|
||||
const names = listing.stdout.trim().split(/\r?\n/).filter((name) => name !== "" && name !== "__.SYMDEF" && name !== "__.SYMDEF SORTED");
|
||||
if (new Set(names).size !== names.length || names.some((name) => !/^[A-Za-z0-9_.-]+\.o$/.test(name))) {
|
||||
throw new RuntimePackError("runtime vendor archive has invalid or duplicate object members", "invalid");
|
||||
}
|
||||
const directory = join(buildDir, `vendor-${index}`);
|
||||
await mkdir(directory);
|
||||
await run(archiver[0]!, [...archiver.slice(1), "x", input, ...names], { cwd: directory });
|
||||
support.push(...names.map((name) => join(directory, name)));
|
||||
}
|
||||
const members = options.localizeSymbols === undefined
|
||||
? [...roots, ...support]
|
||||
: [await localizeLibraryObjects(driver, archiver, buildDir, roots, support, options.localizeSymbols, `${stem}.runtime`, target.platform)];
|
||||
const archive = join(buildDir, "library.a");
|
||||
await run(archiver[0]!, [...archiver.slice(1), "rcs", archive, ...members], {
|
||||
env: { ...process.env, ZERO_AR_DATE: "1" },
|
||||
});
|
||||
await mkdir(dirname(options.outPath), { recursive: true });
|
||||
await installBytes(await readFile(archive), options.outPath);
|
||||
} finally {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
await rm(stage.root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export async function executableLinkerEnvironmentFingerprint(
|
||||
defaultLinker: string = "clang",
|
||||
): Promise<string> {
|
||||
const linker = resolvePlatformLinker(env, defaultLinker);
|
||||
const linkerIdentity = platformLinkerIdentity(env);
|
||||
const linkerIdentity = platformLinkerIdentity(env, linker);
|
||||
let effectiveDriverIdentity: string;
|
||||
try {
|
||||
const selected = await execFileAsync(linker, ["-print-prog-name=clang"], {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { commentText, llvmBytes, llvmQuoted, octalByte, unsignedHex } from "./literals.js";
|
||||
import { cCommentText, cNumberLiteral, cStringLiteral } from "./c/types.js";
|
||||
import { f64Lit, llvmCommentText } from "./llvm/common.js";
|
||||
import { mangleFunction, mangleLocal } from "./mangle.js";
|
||||
|
||||
@@ -22,7 +21,6 @@ test("octal and LLVM escapes cover every byte without ambiguous suffixes", () =>
|
||||
expect(llvmBytes(bytes)).toBe(expected + "\\00");
|
||||
expect(llvmBytes(bytes, false)).toBe(expected);
|
||||
expect(llvmBytes(new Uint8Array())).toBe("\\00");
|
||||
expect(cStringLiteral(Buffer.from([0, 49, 255, 55, 63, 34, 92]))).toBe('"\\0001\\3777\\?\\"\\\\"');
|
||||
});
|
||||
|
||||
test("metadata strings preserve UTF-8 replacement, embedded NUL and quotes", () => {
|
||||
@@ -40,10 +38,9 @@ test("comments preserve ordinary text and encode all source-control units", () =
|
||||
"\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
|
||||
expect(commentText(text)).toBe(expected);
|
||||
expect(llvmCommentText(text)).toBe(expected);
|
||||
expect(cCommentText(text)).toBe(expected.replace(/\*\//g, "* /").replace(/\/\*/g, "/ *"));
|
||||
});
|
||||
|
||||
test("name spelling remains byte-for-byte compatible, including surrogate units", () => {
|
||||
test("name spelling remains stable, including surrogate units", () => {
|
||||
for (const name of ["abc_X9", "x.12", "$x%y", "é日本", "😀", "\ud800\0", "dash-here"]) {
|
||||
const expected = name.replace(/[^A-Za-z0-9_]/g, (ch) => ch === "." ? "_" : `_x${ch.codePointAt(0)!.toString(16)}_`);
|
||||
expect(mangleFunction(name)).toBe("sc_f_" + expected);
|
||||
@@ -57,6 +54,4 @@ test("double spelling preserves every bit, including signed zero and subnormals"
|
||||
bytes.writeDoubleBE(number);
|
||||
expect(f64Lit(number)).toBe("0x" + bytes.toString("hex").toUpperCase());
|
||||
}
|
||||
expect(cNumberLiteral(-0)).toBe("-0.0");
|
||||
expect(cNumberLiteral(2 ** 64)).toBe("18446744073709552000.0");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* The one structure the C emitter never needed: a basic-block builder
|
||||
/* LLVM basic-block builder
|
||||
* lowering the structured statement tree to labeled blocks with explicit
|
||||
* terminators. clang -O0 style — every local is an alloca, every read a
|
||||
* load — so no SSA construction is ever needed; LLVM's mem2reg promotes
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* The LLVM backend's class machinery — the .ll mirror of the C emitter's
|
||||
* class slice (shapes.ts): the class graph (base/children links, the
|
||||
* whole-program preorder numbering behind O(1) instanceof, hierarchy
|
||||
* membership, per-hierarchy virtual slot lists), per-class struct types and
|
||||
* RC/trace helper families, the hierarchy vtable instances, and the class
|
||||
* objects (classes as first-class values) with their construct thunks.
|
||||
*
|
||||
* Two deliberate simplifications over the C emission, both possible because
|
||||
* every pointer is `ptr` in LLVM:
|
||||
* - NO vtable slot adapters: the C backend needs sc_vm_* thunks because a
|
||||
* slot's function-pointer TYPE spells the declaring class's `this`; here
|
||||
* every implementation already has the slot's exact LLVM signature
|
||||
* (override exactness fixes the ABI), so the vtable stores the method
|
||||
* function directly.
|
||||
* - ONE struct spelling per class (%sc_o_*); the runtime error classes GEP
|
||||
* through the emitter-declared %ScrError instead (their structs live in
|
||||
* the runtime).
|
||||
*
|
||||
* Emitter- and stream-rooted classes stay out of the tier (their prefixes
|
||||
* embed runtime registry/state slots and their surfaces are async-shaped);
|
||||
* the emitter refuses them by name before anything here runs. */
|
||||
/** Class layouts, reference counting, hierarchy intervals, virtual tables and
|
||||
* constructor values. Opaque LLVM pointers allow virtual slots to store method
|
||||
* implementations directly. Runtime EventEmitter and stream subclasses embed
|
||||
* their runtime prefixes ahead of user fields. */
|
||||
import type { IrClassDef, IrFunction, IrModule, IrType, IrUnionDef } from "../../ir/ir.js";
|
||||
import { isRefCounted, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES } from "../../ir/ir.js";
|
||||
import { streamRooted, undefinedArmTag } from "../../ir/analysis.js";
|
||||
@@ -49,7 +32,7 @@ export interface LlVtSlot {
|
||||
fn: IrFunction;
|
||||
}
|
||||
|
||||
/** Per-class node of the class graph — the CEmitter ClassMeta shape. */
|
||||
/** Per-class node of the class graph. */
|
||||
export class LlClassMeta {
|
||||
base: LlClassMeta | null = null;
|
||||
children: LlClassMeta[] = [];
|
||||
@@ -66,12 +49,11 @@ export class LlClassMeta {
|
||||
}
|
||||
|
||||
/** The class graph: link base/children, number the forest in preorder
|
||||
* (roots and children in module class order — the SAME numbering the C
|
||||
* backend computes, so instanceof agrees between backends and with the
|
||||
* (roots and children in module class order, matching the
|
||||
* runtime's stamped error vtables), and compute each hierarchy's virtual
|
||||
* slots — a class's method gets a slot iff no ancestor declares it AND some
|
||||
* strict descendant redeclares it (whole-program devirtualization).
|
||||
* Ported from the CEmitter constructor. */
|
||||
* The preorder intervals also identify runtime class membership. */
|
||||
export function buildClassGraph(mod: IrModule, fnByName: Map<string, IrFunction>): Map<string, LlClassMeta> {
|
||||
const metaMap = new Map<string, LlClassMeta>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
@@ -522,8 +504,8 @@ export function emitClassShapes(
|
||||
|
||||
/** Class objects (classes as first-class values): the immortal ScrClassObj
|
||||
* statics plus their construct thunks — allocate, run the constructor over
|
||||
* a +1 `this`, hand the remaining +1 out (the C emitCtorThunkDefs, minus
|
||||
* the pending check: in-tier constructors are throw-free by construction).
|
||||
* a +1 `this`, and hand the remaining +1 out. No pending check is needed:
|
||||
* in-tier constructors are throw-free by construction.
|
||||
* `classObjs` maps className → { nameSym } registered during body emission
|
||||
* (the literal interned while the table was open). */
|
||||
export function emitClassObjDefs(
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll
|
||||
* mirror of walkers.ts's dyn slice: per-type match predicates
|
||||
/* ScrDyn helpers for the LLVM backend: per-type match predicates
|
||||
* (dynMatchHelper), checked builders (dynCheckHelper), static→dyn
|
||||
* converters (toDynHelper), the type-independent singletons (String
|
||||
* (unknown), caught→dyn, the keyed read, the destructuring
|
||||
* RequireObjectCoercible, GetIterator+N), and the checked-dynamic
|
||||
* function boundary's thunk/box/adapter triple. Every helper follows the
|
||||
* C emitter's semantics EXACTLY — same runtime entry points, same
|
||||
* ownership, same path-annotated failure texts — so the differential
|
||||
* suite's byte-parity contract holds through either backend.
|
||||
* function boundary's thunk/box/adapter triple. Helpers use the runtime ABI,
|
||||
* explicit reference ownership, and path-annotated failure messages.
|
||||
*
|
||||
* dyn layout facts this file compiles against (scr_runtime.h):
|
||||
* ScrDyn { size_t rc; ScrDynKind kind; bool buffer; union v; }
|
||||
@@ -1477,7 +1474,7 @@ export class LlDyn {
|
||||
host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
// CYCLE-CAPABLE shapes guard the deep copy: enter TRAPS on a value
|
||||
// already being converted (a cyclic value has no finite dyn copy —
|
||||
// SEMANTICS.md; the C emitter's contract exactly).
|
||||
// SEMANTICS.md).
|
||||
const cyclicRec = traceAdapter(host, t) !== null;
|
||||
if (cyclicRec) {
|
||||
host.declare(`declare void @scr_dyn_from_enter(ptr)`);
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt, type IrType } from "../../ir/ir.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
const loc = { file: "exception-cleanup.ts", start: 0, end: 0 };
|
||||
const call = (): IrStmt => ({ kind: "exprStmt", expr: { kind: "call", callee: "throws", args: [], type: VOID, loc }, loc });
|
||||
const literal = (value: string) => ({ kind: "strLit" as const, value, type: STRING, loc });
|
||||
function moduleFor(body: IrStmt[], options: { boxed?: boolean; numberReturn?: boolean } = {}): IrModule {
|
||||
const params = ["first", "second", "third"].map((name) => ({ name, localId: name, type: STRING }));
|
||||
const work: IrFunction = {
|
||||
name: "work", params, returnType: options.numberReturn ? F64 : VOID,
|
||||
locals: [...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false, ...(options.boxed ? { boxed: true as const } : {}) })),
|
||||
{ id: "later", name: "later", type: STRING, mutable: true }],
|
||||
body: [...body, ...(options.numberReturn ? [{ kind: "return" as const, value: { kind: "numLit" as const, value: 7, type: F64, loc }, loc }] : [])], loc,
|
||||
};
|
||||
return {
|
||||
irVersion: 13, sourceFile: loc.file, entry: "main", functions: [
|
||||
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }, work,
|
||||
{ name: "throws", params: [], returnType: VOID, locals: [], body: [{ kind: "throw", value: literal("oops"), loc }], loc },
|
||||
],
|
||||
};
|
||||
}
|
||||
function cleanupBlocks(module: IrModule): string[] {
|
||||
const llvm = emitLlvmModule(module);
|
||||
return [...llvm.matchAll(/^exc\.cleanup\d+:\n(?: [^\n]*\n)+/gm)].map((match) => match[0]!);
|
||||
}
|
||||
|
||||
test("throwing-call count does not multiply identical scope cleanup", () => {
|
||||
for (const boxed of [false, true]) {
|
||||
const small = emitLlvmModule(moduleFor(Array.from({ length: 4 }, call), { boxed }));
|
||||
const large = emitLlvmModule(moduleFor(Array.from({ length: 64 }, call), { boxed }));
|
||||
const release = boxed ? /call void @scr_box_release\(ptr/g : /call void @scr_str_release\(ptr/g;
|
||||
expect(small.match(release)?.length).toBeGreaterThan(0);
|
||||
expect(large.match(release)?.length).toBe(small.match(release)?.length);
|
||||
}
|
||||
});
|
||||
|
||||
test("cleanup snapshots keep locals declared after an earlier throw separate", () => {
|
||||
const module = moduleFor([call(), { kind: "varDecl", localId: "later", init: literal("created"), loc }, call()]);
|
||||
const blocks = cleanupBlocks(module);
|
||||
expect(blocks).toHaveLength(2);
|
||||
expect(blocks.filter((block) => block.includes("%sc_l_later"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("equivalent slots with different catch destinations remain separate", () => {
|
||||
const module = moduleFor([
|
||||
{ kind: "tryCatch", tryBody: [{ kind: "varDecl", localId: "later", init: literal("one"), loc }, call(), call()], catchBody: [], catchLocalId: null, finallyBody: null, loc },
|
||||
{ kind: "tryCatch", tryBody: [{ kind: "varDecl", localId: "later", init: literal("two"), loc }, call(), call()], catchBody: [], catchLocalId: null, finallyBody: null, loc },
|
||||
]);
|
||||
const blocks = cleanupBlocks(module).filter((block) => block.includes("%sc_l_later"));
|
||||
expect(blocks).toHaveLength(2);
|
||||
const targets = blocks.map((block) => /br label %([^\n]+)/.exec(block)?.[1]);
|
||||
expect(targets.every(Boolean)).toBe(true);
|
||||
expect(new Set(targets).size).toBe(2);
|
||||
});
|
||||
|
||||
test("shared exceptional exits preserve the function's scalar return ABI", () => {
|
||||
const blocks = cleanupBlocks(moduleFor([call(), call()], { numberReturn: true }));
|
||||
expect(blocks).toHaveLength(1);
|
||||
expect(blocks[0]).toContain("ret double 0x0000000000000000");
|
||||
});
|
||||
|
||||
const receiverLoc = { file: "read-receiver.ts", start: 0, end: 0 };
|
||||
const leaf: IrType = { kind: "record", shapeId: "leaf" };
|
||||
const root: IrType = { kind: "record", shapeId: "root" };
|
||||
const union: IrType = { kind: "union", unionId: "value" };
|
||||
const ref = (type: IrType): IrExpr => ({ kind: "varRef", localId: "value", type, loc: receiverLoc });
|
||||
const narrow = (value: IrExpr): IrExpr => ({ kind: "unionNarrow", unionId: "value", tag: 0, value, type: root, loc: receiverLoc });
|
||||
const child = (obj: IrExpr): IrExpr => ({ kind: "recordGet", obj, shapeId: "root", field: "child", type: leaf, loc: receiverLoc });
|
||||
const text = (obj: IrExpr): IrExpr => ({ kind: "recordGet", obj, shapeId: "leaf", field: "text", type: STRING, loc: receiverLoc });
|
||||
|
||||
function work(expr: IrExpr, parameter: IrType, boxed = false): string {
|
||||
const module: IrModule = {
|
||||
irVersion: 13, sourceFile: receiverLoc.file, entry: "main",
|
||||
records: [
|
||||
{ id: "leaf", fields: [{ name: "text", type: STRING }] },
|
||||
{ id: "root", fields: [{ name: "child", type: leaf }] },
|
||||
],
|
||||
unions: [{ id: "value", arms: [root, STRING] }],
|
||||
functions: [
|
||||
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc: receiverLoc },
|
||||
{ name: "work", params: [{ name: "value", localId: "value", type: parameter }], returnType: expr.type,
|
||||
locals: [{ id: "value", name: "value", type: parameter, mutable: false, ...(boxed ? { boxed: true } : {}) }],
|
||||
body: [{ kind: "return", value: expr, loc: receiverLoc }], loc: receiverLoc },
|
||||
],
|
||||
};
|
||||
const llvm = emitLlvmModule(module);
|
||||
const body = /^define internal [^\n]*@sc_f_work\([^]*?^}/m.exec(llvm)?.[0];
|
||||
expect(body).toBeDefined();
|
||||
return body!;
|
||||
}
|
||||
|
||||
test("nested union and record projections retain only the escaping reference", () => {
|
||||
const llvm = work(text(child(narrow(ref(union)))), union);
|
||||
expect(llvm).not.toContain("call ptr @scr_union_retain_v");
|
||||
expect(llvm).not.toMatch(/call ptr @sc_rretain_/);
|
||||
expect(llvm.match(/call ptr @scr_str_retain_v/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("tag tests read an owned local without adding a temporary owner", () => {
|
||||
const llvm = work({ kind: "unionIsTag", unionId: "value", value: ref(union), tag: 0, negated: false, type: BOOL, loc: receiverLoc }, union);
|
||||
expect(llvm).not.toContain("call ptr @scr_union_retain_v");
|
||||
expect(llvm).toContain("icmp eq i32");
|
||||
});
|
||||
|
||||
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");
|
||||
expect(llvm).toContain("call void @scr_union_release");
|
||||
expect(llvm.match(/call ptr @scr_str_retain_v/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
function sharedFieldModule(prefixes: IrType[][], fieldType: IrType): IrModule {
|
||||
const shared: IrType = { kind: "union", unionId: "shared" };
|
||||
const records = prefixes.map((prefix, index) => ({
|
||||
id: `variant${index}`,
|
||||
fields: [...prefix.map((type, field) => ({ name: `prefix${field}`, type })),
|
||||
{ name: "value", type: fieldType }, { name: `tail${index}`, type: index % 2 ? BOOL : F64 }],
|
||||
}));
|
||||
return {
|
||||
irVersion: 13, sourceFile: loc.file, entry: "main", records,
|
||||
unions: [{ id: "shared", arms: records.map((record) => ({ kind: "record", shapeId: record.id })) }],
|
||||
functions: [
|
||||
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
{ name: "read", params: [{ localId: "value", name: "value", type: shared }], returnType: fieldType,
|
||||
locals: [{ id: "value", name: "value", type: shared, mutable: false }], loc,
|
||||
body: [{ kind: "return", value: { kind: "unionDisc", value: ref(shared), unionId: "shared", field: "value", type: fieldType, loc }, loc }] },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("wide unions share field reads with identical storage prefixes", () => {
|
||||
for (const pointerBits of [32, 64] as const) {
|
||||
const llvm = emitLlvmModule(sharedFieldModule(Array.from({ length: 128 }, () => [BOOL, STRING]), STRING), { pointerBits });
|
||||
const body = /^define internal [^\n]*@sc_f_read\([^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).not.toContain("switch i32");
|
||||
expect(body).toMatch(/icmp ult i32 %\w+, 128/);
|
||||
expect(body).toContain("call void @sc_bad_tag()");
|
||||
expect(body.match(/call ptr @scr_str_retain_v/g)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("different union field offsets retain per-variant dispatch", () => {
|
||||
for (const prefixes of [[[], [BOOL]], [[BOOL], [F64]], [[STRING], [BOOL]]]) {
|
||||
const llvm = emitLlvmModule(sharedFieldModule(prefixes, STRING));
|
||||
const body = /^define internal [^\n]*@sc_f_read\([^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).toContain("switch i32");
|
||||
expect(body.match(/call ptr @scr_str_retain_v/g)).toHaveLength(2);
|
||||
}
|
||||
});
|
||||
|
||||
test("partially shared union layouts emit one read per storage prefix", () => {
|
||||
const llvm = emitLlvmModule(sharedFieldModule(Array.from({ length: 128 }, (_, index) => index % 2 ? [BOOL] : []), STRING));
|
||||
const body = /^define internal [^\n]*@sc_f_read\([^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).toContain("switch i32");
|
||||
expect(body.match(/call ptr @scr_str_retain_v/g)).toHaveLength(2);
|
||||
const destinations = [...body.matchAll(/i32 \d+, label %(u\.a\d+)/g)].map(match => match[1]);
|
||||
expect(destinations).toHaveLength(128);
|
||||
expect(new Set(destinations).size).toBe(2);
|
||||
});
|
||||
|
||||
test("shared boolean union fields keep their byte storage and scalar result", () => {
|
||||
const llvm = emitLlvmModule(sharedFieldModule([[], []], BOOL));
|
||||
const body = /^define internal [^\n]*@sc_f_read\([^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).not.toContain("switch i32");
|
||||
expect(body.match(/load i8, ptr/g)).toHaveLength(1);
|
||||
expect(body).toMatch(/trunc i8 %\w+ to i1/);
|
||||
});
|
||||
|
||||
test("literal zero division emits the JavaScript NaN constant in development builds", () => {
|
||||
const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc: receiverLoc };
|
||||
const llvm = work({ kind: "bin", op: "/", left: zero, right: zero, type: F64, loc: receiverLoc }, F64);
|
||||
expect(llvm).not.toContain("fdiv");
|
||||
expect(llvm).toContain("ret double 0x7FF8000000000000");
|
||||
const dynamic = work({ kind: "bin", op: "/", left: ref(F64), right: ref(F64), type: F64, loc: receiverLoc }, F64);
|
||||
expect(dynamic).toContain("fdiv double");
|
||||
});
|
||||
@@ -1,67 +1,22 @@
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { llvmBytes as llBytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* IR → LLVM IR text (.ll). The LLVM backend consumes the SAME in-memory
|
||||
* IrModule the C backend does (never the JSON dump — see the -0 lesson in
|
||||
* the survey). Its textual module is either lowered by scriptc's native
|
||||
* helper and linked with a runtime pack or occupies the legacy compiler
|
||||
* driver's program-TU seat. Both paths use the same scr_* C ABI.
|
||||
/** Lower typed IR to LLVM, using the runtime's C ABI.
|
||||
*
|
||||
* Phase 1 was the TRIVIAL TIER: f64/bool/string locals and params, the
|
||||
* scalar operator set, structured control flow, direct calls, interned
|
||||
* string literals, and the console protocol. Phase 2 adds the VOLUME
|
||||
* TIER: module globals and locals of every in-tier ref kind (arrays,
|
||||
* record shapes, unions, function values/closures), the full array and
|
||||
* string intrinsic surfaces, per-record-shape RC helpers (cycle headers
|
||||
* included — the C emitter's fixpoint, ported in shapes.ts), tagged-union
|
||||
* construction/narrowing/equality with interned immortal unit instances,
|
||||
* capture boxes and the closure calling convention, switch/for-of
|
||||
* lowering, and the non-throwing slice of the libCall table. EVERYTHING
|
||||
* ELSE REFUSES loudly at the first unhandled node (LlvmUnsupportedError
|
||||
* naming the kind) — this backend never guesses and never emits wrong
|
||||
* code for a construct it does not model. compile() surfaces the refusal
|
||||
* as diagnostic SC3001.
|
||||
* Reference-counted temporaries own one reference. Declarations, assignments,
|
||||
* returns and arguments move that ownership; frame and scope exits release
|
||||
* everything remaining. Slot-backed entries release the slot's current value.
|
||||
*
|
||||
* RC ownership discipline: the frame/scope release-point machinery is
|
||||
* ported from CEmitter (docs/ir.md) — every refcounted temp holds an owned
|
||||
* +1 reference; varDecl/assign/return/call-argument MOVE that ownership;
|
||||
* each statement releases its remaining refcounted temps when it ends;
|
||||
* each scope releases the refcounted locals declared in it when it exits;
|
||||
* callees own their params; return/break/continue release everything the
|
||||
* jump bypasses (releaseForJump). Releases are type-directed through
|
||||
* shapes.ts (the releaseCallC table's LLVM twin); frame entries can be
|
||||
* SLOT-based (the entry names a pointer whose CURRENT value releases —
|
||||
* conditional results like optional chains need that indirection).
|
||||
* Exceptions use a pending flag and explicit cleanup edges. Throwing calls
|
||||
* test scr_exc_pending before consuming results, release the scopes crossed,
|
||||
* and branch to the innermost handler or return to the caller. Catch bindings
|
||||
* hold ScrCaught snapshots; finally runs on normal, throwing and returning
|
||||
* paths. This keeps ownership cleanup intact without nonlocal jumps.
|
||||
*
|
||||
* Exceptions (phase 4): the pending-flag unwind protocol, ported from the
|
||||
* C emitter. `throw` moves its payload into the runtime's exception cell
|
||||
* (scr_throw_*) and unwinds; after every call that can raise (per the
|
||||
* SAME computeMayThrow analysis the C backend runs) a pending check tests
|
||||
* scr_exc_pending() and unwinds — releasing frames/scopes down to the
|
||||
* innermost try handler's depths and branching to its label, or releasing
|
||||
* everything and returning a dummy value (never read: callers of a
|
||||
* may-throw function test the flag before using the result). No
|
||||
* setjmp/longjmp: longjmp would skip the emitted RC releases. try/catch
|
||||
* follows stmts.ts's shape exactly — a compile-time tryStack entry
|
||||
* per region, the catch block taking the exception (scr_exc_take into the
|
||||
* binding's snapshot box, or scr_exc_clear for the bindingless form), the
|
||||
* finally body emitted once per path (normal, exception-with-stash,
|
||||
* pending-return) with fresh temps each time. Catch bindings ride
|
||||
* ScrCaught snapshot boxes (caughtTest/caughtNarrow/caughtCheck read
|
||||
* them); TDZ reads test the box's payload slot and throw Node's
|
||||
* ReferenceError. main() gains the uncaught epilogue when the entry
|
||||
* function may throw.
|
||||
*
|
||||
* The dyn surface (phase 5): ScrDyn dyn values are in the tier — dyn.ts
|
||||
* ports walkers.ts's dyn slice (match/check/toDyn walkers, the
|
||||
* String(unknown)/caught→dyn/keyed-read singletons, the checked-dynamic
|
||||
* function boundary's thunk/box/adapter triple) and the emitter lowers
|
||||
* the dyn expression kinds (dynFrom/dynCall/dynInvoke/dynTest/dynKeyGet/
|
||||
* dynCheck/destructuring), the JSON.parse family, dyn record fields and
|
||||
* overflow maps, dyn capture boxes, and generator unknown channels.
|
||||
* The island surface (jsval/jsExit and embedded npm tables) is in the
|
||||
* tier too; the module text and resolution tables use the same compressed,
|
||||
* lazy-inflate representation as the C debugging backend.
|
||||
* Program objects are emitted by the packaged LLVM helper and linked with
|
||||
* runtime packs. Sanitizer development builds use an external LLVM driver.
|
||||
* Dynamic values and islands share the runtime ABI; embedded npm modules use
|
||||
* compressed source tables that the runtime inflates on demand.
|
||||
*/
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import { endsWithJump, matchStringSelfConcat, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
@@ -81,14 +36,14 @@ import type {
|
||||
IrUnionDef,
|
||||
SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js";
|
||||
import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleRuntimeFeatures, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js";
|
||||
import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js";
|
||||
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { analyzeIntegerRanges, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
|
||||
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
|
||||
import { computeMayThrow } from "../c/may-throw.js";
|
||||
import { computeMayThrow } from "../may-throw.js";
|
||||
import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleClassStruct, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
@@ -217,11 +172,11 @@ export class LlEmitter {
|
||||
private readonly emitLibraryIdentity: boolean;
|
||||
private readonly runtimeAbiMarker: boolean;
|
||||
/** Interned string literals: UTF-8 text → { symbol, byte length } —
|
||||
* first-use order, the C emitter's determinism discipline. */
|
||||
* first-use order, the runtime ABI’s determinism discipline. */
|
||||
private readonly literals = new Map<string, { sym: string; len: number }>();
|
||||
/** Interned unit-armed union instances: "unionId:tag" → symbol — one
|
||||
* immortal (rc == SIZE_MAX) static per (union, unit tag), exactly the
|
||||
* C emitter's table. RC entry points and the collector skip immortals. */
|
||||
* immortal (rc == SIZE_MAX) static per (union, unit tag).
|
||||
* RC entry points and the collector skip immortals. */
|
||||
private readonly unitInstances = new Map<string, string>();
|
||||
/** Interned regex literals: "<flags>/<pattern>" → { symbol, interned
|
||||
* source/flags literal refs } — one immortal ScrRegex per distinct
|
||||
@@ -271,18 +226,18 @@ export class LlEmitter {
|
||||
private readonly globalTypes = new Map<string, IrType>();
|
||||
private readonly tdzGlobals = new Map<string, string>();
|
||||
readonly constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
|
||||
/** May-throw analysis (the C emitter's computeMayThrow, shared): pending
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
readonly indirectMayThrow: boolean;
|
||||
/** Method names with at least one may-throw implementation — the
|
||||
* virtualCall pending check's key (CEmitter.mayThrowMethods). */
|
||||
* virtualCall pending check's key. */
|
||||
readonly mayThrowMethods = new Set<string>();
|
||||
/** setTimeout and friends appeared somewhere: main must run the event
|
||||
* loop even in programs with no async functions (CEmitter.usesTimers). */
|
||||
* loop even in programs with no async functions. */
|
||||
usesTimers = false;
|
||||
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
|
||||
* typeKey → thunk symbol (CEmitter.resolveThunks). */
|
||||
* typeKey → thunk symbol. */
|
||||
readonly resolveThunks = new Map<string, string>();
|
||||
readonly resolveThunkDefs: string[] = [];
|
||||
/** ReadableStream.from adapters keep typed arrays by reference and box
|
||||
@@ -303,15 +258,16 @@ export class LlEmitter {
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
/** The class graph (buildClassGraph): preorder numbering, hierarchy
|
||||
* membership, virtual slot lists — the CEmitter classMeta, ported. */
|
||||
* membership, virtual slot lists. */
|
||||
readonly classMeta: Map<string, LlClassMeta>;
|
||||
/** Class objects (classes as first-class values): className → the
|
||||
* interned .name literal ref — registered during body emission, the
|
||||
* statics and construct thunks assemble around the bodies. */
|
||||
private readonly classObjs = new Map<string, { nameSym: string }>();
|
||||
private readonly unionFieldReadGroups = new Map<string, number[][]>();
|
||||
/** Preorder intervals of the runtime error classes under THIS module's
|
||||
* class-forest numbering (main() stamps scr_error_vts with them, exactly
|
||||
* like the C emitter's errorVtStampLines). */
|
||||
* like the runtime ABI’s errorVtStampLines). */
|
||||
private readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = [];
|
||||
/** The runtime emitter vtable's preorder interval, when the program
|
||||
* touches node:events (the class def rides the module exactly then) —
|
||||
@@ -327,6 +283,10 @@ export class LlEmitter {
|
||||
B = new BlockBuilder();
|
||||
frames: LlValue[][] = [];
|
||||
private scopes: LlScopeEntry[][] = [];
|
||||
/** Scope slots dominate every throw site. Share their exceptional cleanup
|
||||
* for identical live bindings and handlers; statement temporaries stay at
|
||||
* their own throw sites because their SSA values are path-dependent. */
|
||||
private readonly unwindCleanups = new Map<string, { label: string; entries: LlScopeEntry[]; terminator: string }>();
|
||||
/** Enclosing break/continue targets. `kind` separates loops from
|
||||
* switches and labeled blocks: an unlabeled break binds to the innermost
|
||||
* NON-BLOCK entry (loop or switch — blocks only enter the stack when
|
||||
@@ -350,7 +310,7 @@ export class LlEmitter {
|
||||
integerRanges: IntegerRanges = new Map();
|
||||
/** Enclosing try-with-FINALLY regions, innermost last: a `return`
|
||||
* inside one runs every crossed finally (innermost first) before the
|
||||
* actual ret — the C emitter's pending-return path, with the finally
|
||||
* actual ret — the runtime ABI’s pending-return path, with the finally
|
||||
* bodies emitted inline at the return site instead of behind a goto.
|
||||
* `tryDepth` snapshots tryStack.length at region entry: a throw inside
|
||||
* a pending-return finally copy propagates OUT of the completing try
|
||||
@@ -359,7 +319,7 @@ export class LlEmitter {
|
||||
* emitFinallysForJump. */
|
||||
private finallyStack: { frameDepth: number; scopeDepth: number; tryDepth: number; body: IrStmt[] }[] = [];
|
||||
/** Enclosing try contexts, innermost last — the compile-time unwind
|
||||
* targets (CEmitter.tryStack): a pending check or `throw` inside a try
|
||||
* targets: a pending check or `throw` inside a try
|
||||
* releases frames/scopes down to the recorded depths and branches to
|
||||
* `label` (the catch, or the exception-path finally) instead of
|
||||
* returning out of the function. Entering a try emits no code. */
|
||||
@@ -440,7 +400,7 @@ export class LlEmitter {
|
||||
// Module globals: scalar (f64/bool) storage is a zero-initialized
|
||||
// LLVM global, ref-kind storage a null-initialized ptr — load/store
|
||||
// like a local, assigned by the %init functions. Refcounted globals
|
||||
// are released at the end of main (the C emitter's
|
||||
// are released at the end of main (the runtime ABI’s
|
||||
// sc_release_globals), before the RC audit would run.
|
||||
try {
|
||||
this.llType(g.type); // refuses out-of-tier kinds
|
||||
@@ -451,11 +411,8 @@ export class LlEmitter {
|
||||
this.globalTypes.set(g.id, g.type);
|
||||
if (g.tdz) this.tdzGlobals.set(g.id, g.name);
|
||||
}
|
||||
// User classes are IN the tier (phase 3), and so are the runtime
|
||||
// error classes and the runtime EventEmitter/stream classes (phase 6
|
||||
// — subclasses embed the ScrEmitter/ScrStream prefixes, classes.ts).
|
||||
// Anything else runtime-flagged refuses by name, exactly the
|
||||
// classDef:* histogram key.
|
||||
// Runtime error, EventEmitter, and stream classes have known layouts.
|
||||
// Subclasses embed the ScrEmitter/ScrStream prefixes from classes.ts.
|
||||
const classes = mod.classes ?? [];
|
||||
for (const cls of classes) {
|
||||
if (
|
||||
@@ -468,9 +425,8 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
// The class graph: base/children links, hierarchy membership, the
|
||||
// whole-program preorder numbering (identical to CEmitter's, so
|
||||
// runtime-made and compiled error objects agree on instanceof through
|
||||
// either backend), and the per-hierarchy virtual slot lists.
|
||||
// whole-program preorder numbering for instanceof over runtime and
|
||||
// compiled error objects, and the per-hierarchy virtual slot lists.
|
||||
this.classMeta = buildClassGraph(mod, this.fnByName);
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = this.classMeta.get(name);
|
||||
@@ -498,7 +454,7 @@ export class LlEmitter {
|
||||
// ── types ───────────────────────────────────────────────────────────────
|
||||
|
||||
llType(t: IrType): string {
|
||||
if (POINTER_KINDS.has(t.kind) && t.kind !== "http2Session" && t.kind !== "http2Stream") return "ptr";
|
||||
if (POINTER_KINDS.has(t.kind)) return "ptr";
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
@@ -844,7 +800,7 @@ export class LlEmitter {
|
||||
|
||||
emit(): string {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the C emitter's order.
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
const fnDefs: string[] = [];
|
||||
for (const fn of this.mod.functions) fnDefs.push(this.emitFunction(fn));
|
||||
const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t));
|
||||
@@ -885,17 +841,18 @@ export class LlEmitter {
|
||||
seenGlobalIds.add(g.id);
|
||||
return true;
|
||||
});
|
||||
// Refcounted globals release before main returns — the C emitter's
|
||||
// Refcounted globals release before main returns — the runtime ABI’s
|
||||
// sc_release_globals, keeping the RC audit's live count exact. Built
|
||||
// before the declaration table flushes (it adds the release symbols).
|
||||
// Two spellings with distinct temp names: the normal exit and the
|
||||
// uncaught-exception exit are separate blocks of the same function.
|
||||
// Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so
|
||||
// an own-property table Object.defineProperties hung on one would
|
||||
// outlive the RC audit — release it with the globals (the C emitter's
|
||||
// outlive the RC audit — release it with the globals (the runtime ABI’s
|
||||
// sc_release_globals tail). Only when the dispatch unit is even
|
||||
// linked (defineProps is the only writer).
|
||||
const fnValueProps = moduleUsesDynInvoke(this.mod) ? [...this.fnValues] : [];
|
||||
const runtimeFeatures = moduleRuntimeFeatures(this.mod);
|
||||
const fnValueProps = runtimeFeatures.dynInvoke ? [...this.fnValues] : [];
|
||||
if (fnValueProps.length > 0) this.declare(`declare void @scr_box_release(ptr)`);
|
||||
const globalReleaseLines = (prefix: string): string[] => {
|
||||
const lines: string[] = [];
|
||||
@@ -918,26 +875,28 @@ export class LlEmitter {
|
||||
return lines;
|
||||
};
|
||||
// Exit listeners can read MODULE GLOBALS directly, so they must run
|
||||
// BEFORE the global releases (the C emitter's runExitListeners
|
||||
// BEFORE the global releases (the runtime ABI’s runExitListeners
|
||||
// ordering — the atexit half becomes an idempotent no-op).
|
||||
const usesEvents = moduleUsesProcessEvents(this.mod);
|
||||
const usesChildProcess = moduleUsesChildProcess(this.mod);
|
||||
const usesFsWatch = moduleUsesFsWatch(this.mod);
|
||||
const usesEvents = runtimeFeatures.processEvents;
|
||||
const usesChildProcess = runtimeFeatures.childProcess;
|
||||
const usesFsWatch = runtimeFeatures.fsWatch;
|
||||
// Stream-surface programs fill the loop's stream hook (the deferred
|
||||
// next-tick emissions) and the emitter's post-registration flow kick
|
||||
// before %main — scr_stream.c links only when the line is emitted
|
||||
// (native-toolchain.ts gates on the same predicate).
|
||||
const usesStream = moduleUsesStream(this.mod);
|
||||
const usesStream = runtimeFeatures.stream;
|
||||
// Net-surface programs fill the loop's net hooks (and the netSocket
|
||||
// handle-dispatch ops for the checked-dynamic boundary); http-surface
|
||||
// programs additionally stamp the httpReq/httpRes ops — the C main's
|
||||
// install lines, gated on the same predicates native-toolchain.ts links by.
|
||||
const usesNet = moduleUsesNet(this.mod);
|
||||
const usesHttp = moduleUsesHttpServer(this.mod);
|
||||
const usesNet = runtimeFeatures.net;
|
||||
const usesDgram = runtimeFeatures.dgram;
|
||||
const usesHttp2 = runtimeFeatures.http2;
|
||||
const usesHttp = runtimeFeatures.http;
|
||||
// Fetch-referencing programs register the native fetch bridge before
|
||||
// any island entry (the engine's lazy boot consults it) — native-toolchain.ts
|
||||
// compiles scr_fetch.c on the same predicate.
|
||||
const usesFetch = moduleUsesFetch(this.mod);
|
||||
const usesFetch = runtimeFeatures.fetch;
|
||||
const embedsZlib = moduleEmbedsBuiltin(this.mod, "node:zlib");
|
||||
const embedsNet =
|
||||
moduleEmbedsBuiltin(this.mod, "node:http") ||
|
||||
@@ -945,13 +904,13 @@ export class LlEmitter {
|
||||
moduleEmbedsBuiltin(this.mod, "node:net") ||
|
||||
moduleEmbedsBuiltin(this.mod, "node:tls");
|
||||
const snapshotsTlsCa =
|
||||
moduleUsesTls(this.mod) || moduleUsesTlsCa(this.mod) ||
|
||||
runtimeFeatures.tls || runtimeFeatures.tlsCa ||
|
||||
moduleEmbedsBuiltin(this.mod, "node:https") ||
|
||||
moduleEmbedsBuiltin(this.mod, "node:tls");
|
||||
// The process verdict has the same precedence as the C reference
|
||||
// emitter: node:test owns the final status when present; otherwise an
|
||||
// embedded process.exitCode owns it; ordinary programs return zero.
|
||||
const usesNodeTest = moduleUsesNodeTest(this.mod);
|
||||
const usesNodeTest = runtimeFeatures.nodeTest;
|
||||
const programExitUsesIsland = !usesNodeTest && usesIsland;
|
||||
// Declared NOW — the extern block flushes before main assembles.
|
||||
if (usesEvents) this.declare(`declare void @scr_events_install()`);
|
||||
@@ -963,6 +922,8 @@ export class LlEmitter {
|
||||
this.declare(`declare void @scr_net_install()`);
|
||||
this.declare(`declare void @scr_net_dyn_install()`);
|
||||
}
|
||||
if (usesDgram) this.declare(`declare void @scr_dgram_install()`);
|
||||
if (usesHttp2) this.declare(`declare void @scr_http2_dyn_install()`);
|
||||
if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`);
|
||||
if (usesFetch) this.declare(`declare void @scr_fetch_install()`);
|
||||
if (embedsZlib) this.declare(`declare void @scr_zlib_island_install()`);
|
||||
@@ -983,7 +944,7 @@ export class LlEmitter {
|
||||
// the refcounted-global releases, or the retained-FFI atexit ledger
|
||||
// sweep (a listener may legitimately release or pump a registration,
|
||||
// and only the inline call orders ahead of every atexit handler —
|
||||
// the C emitter's runExitListeners stance). Plain event programs
|
||||
// the runtime ABI’s runExitListeners stance). Plain event programs
|
||||
// with neither keep the atexit path, so their listener timing is
|
||||
// unchanged.
|
||||
const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0;
|
||||
@@ -1229,7 +1190,7 @@ export class LlEmitter {
|
||||
}
|
||||
for (const [text, lit] of this.literals) {
|
||||
// Immortal interned ScrStr: { rc = SIZE_MAX, len, cap = len, bytes\0 } —
|
||||
// the C emitter's static table, retain/release skip rc == SIZE_MAX.
|
||||
// the runtime ABI’s static table, retain/release skip rc == SIZE_MAX.
|
||||
out.push(
|
||||
`@${lit.sym} = internal global { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, [${lit.len + 1} x i8] } ` +
|
||||
`{ ${this.sizeType} -1, ${this.sizeType} ${lit.len}, ${this.sizeType} ${lit.len}, [${lit.len + 1} x i8] c"${llStrBytes(text)}" }`,
|
||||
@@ -1273,8 +1234,7 @@ export class LlEmitter {
|
||||
}
|
||||
if (this.templateStringsInstances.size > 0) out.push(``);
|
||||
for (const [text, c] of this.cstrs) {
|
||||
// NUL-terminated byte-array constants: the scr_jb_puts / indent-text
|
||||
// currency (the C emitter passes string literals; these are theirs).
|
||||
// NUL-terminated byte-array constants for scr_jb_puts and indentation.
|
||||
out.push(
|
||||
`@${c.sym} = internal constant [${c.len + 1} x i8] c"${llStrBytes(text)}"`,
|
||||
);
|
||||
@@ -1347,9 +1307,8 @@ export class LlEmitter {
|
||||
|
||||
// main(): scr_init, the program-dependent error-vt interval stamps,
|
||||
// scr_lib_init(argc, argv), then the entry function. An uncaught
|
||||
// exception escaping top-level code prints and exits 1 (Node) — the
|
||||
// C emitter's epilogue, emitted exactly when the entry may throw.
|
||||
// No event loop yet: timers/async still refuse (phase 5).
|
||||
// exception escaping top-level code prints and exits 1 (Node).
|
||||
// The exception epilogue is emitted when the entry may throw.
|
||||
const stamps: string[] = [];
|
||||
for (const iv of this.errorIntervals) {
|
||||
const fields: [number, number][] = [[0, iv.pre], [1, iv.post]];
|
||||
@@ -1398,7 +1357,7 @@ export class LlEmitter {
|
||||
}
|
||||
if (this.mod.lib !== undefined) {
|
||||
// LIBRARY mode: no @main — the profile-declared external
|
||||
// symbols instead, from the same IR facts the C emission consumes.
|
||||
// symbols specified by the library IR instead.
|
||||
out.push(...this.emitLibDefs(globals, globalReleaseLines, stamps));
|
||||
out.push(`attributes #0 = { sanitize_address }`);
|
||||
if (this.wasi) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`);
|
||||
@@ -1428,6 +1387,8 @@ export class LlEmitter {
|
||||
...(embedsNet ? [` call void @scr_net_island_install()`] : []),
|
||||
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
|
||||
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
|
||||
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
|
||||
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
|
||||
...(usesStream ? [` call void @scr_stream_install()`] : []),
|
||||
` call void @scr_lib_init(i32 %argc, ptr %argv)`,
|
||||
...(usesIsland
|
||||
@@ -1577,8 +1538,7 @@ export class LlEmitter {
|
||||
// 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
|
||||
// declares text for — the same data the C emission defines, so the
|
||||
// funnel-assembled sink message is emission-invariant by construction.
|
||||
// declares text for. The funnel assembles the structured sink message.
|
||||
// The empty table still defines the symbols the funnel links against.
|
||||
const ovlCells: string[] = [];
|
||||
lib.trapOverlays.forEach((o, i) => {
|
||||
@@ -1636,8 +1596,8 @@ export class LlEmitter {
|
||||
// Host-callback channels: the per-channel name constants (the
|
||||
// registration dispatch's strcmp operands), the per-channel
|
||||
// unregistered-call trap constants (the ffiCall sites'
|
||||
// scr_library_cb_require operands — same bytes as the C emission by
|
||||
// construction), and the registration define: a pure store dispatch
|
||||
// scr_library_cb_require operands), and the registration function:
|
||||
// a pure store dispatch
|
||||
// (the sink registration's rule — no entry prologue, no poison
|
||||
// guard) whose first operation rejects callback-time re-entry
|
||||
// (SC4026). An unknown or NULL name is a defined -1, never a store.
|
||||
@@ -1675,7 +1635,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("llvm", lib.identity, FN_ATTRS));
|
||||
out.push(...emitLibraryIdentityLines(lib.identity, FN_ATTRS));
|
||||
}
|
||||
if (lib.resultResetSymbol !== null) {
|
||||
out.push(
|
||||
@@ -1706,8 +1666,7 @@ export class LlEmitter {
|
||||
if (e.inboundBytesTrap !== undefined) {
|
||||
// The bytes-in helper's trap message: the compiler-assembled
|
||||
// structured trap-teaching form (0x01 text 0x1F SC4012 0x1F symbol
|
||||
// [0x1F remediation]) — the same bytes the C emission passes, so
|
||||
// the sink message is emission-invariant by construction.
|
||||
// [0x1F remediation]) consumed by the runtime sink.
|
||||
const trapBytes = Buffer.byteLength(e.inboundBytesTrap, "utf8");
|
||||
out.push(`@sc_lib_bytes_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundBytesTrap)}"`, ``);
|
||||
}
|
||||
@@ -1842,7 +1801,7 @@ export class LlEmitter {
|
||||
|
||||
/** The shared abort helpers (emitted only when referenced): the OOM
|
||||
* abort of untraced shape allocation and the invalid-union-tag abort —
|
||||
* both print the C emitter's exact message on fd 2 and abort. */
|
||||
* both print the runtime ABI’s exact message on fd 2 and abort. */
|
||||
private helperDefs(): string[] {
|
||||
const defs: string[] = [];
|
||||
const msgHelper = (fnName: string, msgSym: string, msg: string): void => {
|
||||
@@ -1905,7 +1864,7 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
/** Env-signature wrappers + interned immortal closures for declared
|
||||
* functions used as values (the C emitter's sc_w_/sc_fc_ pair): every
|
||||
* functions used as values (the runtime ABI’s sc_w_/sc_fc_ pair): every
|
||||
* mention of `f` yields the same pointer, so `f === f` holds. */
|
||||
private emitFnValueDefs(): string[] {
|
||||
const out: string[] = [];
|
||||
@@ -2304,7 +2263,7 @@ export class LlEmitter {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── plumbing (the CEmitter frame/scope machinery, alloca-flavored) ──────
|
||||
// ── plumbing (frame and scope ownership) ──────
|
||||
|
||||
internLiteral(text: string): string {
|
||||
let lit = this.literals.get(text);
|
||||
@@ -2434,7 +2393,7 @@ export class LlEmitter {
|
||||
/** THE release-on-jump path (break/continue/return): pending statement
|
||||
* frames and entered scopes down to the given depths, innermost first —
|
||||
* everything whose normal fall-through releases the jump bypasses.
|
||||
* Ported verbatim from CEmitter.releaseForJump. */
|
||||
* Release every scope crossed by the control-flow edge. */
|
||||
private releaseForJump(frameDepth: number, scopeDepth: number): void {
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!);
|
||||
for (let i = this.scopes.length - 1; i >= scopeDepth; i--) this.releaseScope(this.scopes[i]!);
|
||||
@@ -2473,25 +2432,44 @@ export class LlEmitter {
|
||||
* function — and branch to the handler / return a dummy value (never
|
||||
* read: callers of a may-throw function test the pending flag before
|
||||
* using the result). Callers own the surrounding pending branch; a
|
||||
* `throw` unwinds unconditionally. CEmitter.emitUnwind, block-flavored. */
|
||||
* `throw` unwinds unconditionally. */
|
||||
private emitUnwind(): void {
|
||||
const target = this.tryStack[this.tryStack.length - 1];
|
||||
const frameDepth = target?.frameDepth ?? 0;
|
||||
const scopeDepth = target?.scopeDepth ?? 0;
|
||||
for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!);
|
||||
let terminator: string;
|
||||
if (target) {
|
||||
this.releaseForJump(target.frameDepth, target.scopeDepth);
|
||||
target.used = true;
|
||||
this.B.terminate(`br label %${target.label}`);
|
||||
terminator = `br label %${target.label}`;
|
||||
} else if (this.currentWasiCoro !== null) {
|
||||
terminator = `br label %${this.currentWasiCoro.finalLabel}`;
|
||||
} else {
|
||||
const t = this.currentReturnType;
|
||||
terminator = t.kind === "void" ? "ret void"
|
||||
: t.kind === "f64" || t.kind === "date" ? `ret double ${f64Lit(0)}`
|
||||
: t.kind === "bool" ? "ret i1 false" : "ret ptr null";
|
||||
}
|
||||
// Copy the entries now: later declarations extend these lexical scopes.
|
||||
// Release order and the handler are part of the shared block's identity.
|
||||
const entries: LlScopeEntry[] = [];
|
||||
let key = terminator;
|
||||
for (let i = this.scopes.length - 1; i >= scopeDepth; i--) {
|
||||
for (const entry of this.scopes[i]!) {
|
||||
entries.push(entry);
|
||||
key += `\0${entry.slot}\0${entry.boxed ? "@scr_box_release" : releaseSym(this.shapeHost, entry.type)}`;
|
||||
}
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
this.B.terminate(terminator);
|
||||
return;
|
||||
}
|
||||
this.releaseForJump(0, 0);
|
||||
if (this.currentWasiCoro !== null) {
|
||||
this.B.terminate(`br label %${this.currentWasiCoro.finalLabel}`);
|
||||
return;
|
||||
let cleanup = this.unwindCleanups.get(key);
|
||||
if (!cleanup) {
|
||||
cleanup = { label: this.B.newLabel("exc.cleanup"), entries, terminator };
|
||||
this.unwindCleanups.set(key, cleanup);
|
||||
}
|
||||
const t = this.currentReturnType;
|
||||
if (t.kind === "void") this.B.terminate("ret void");
|
||||
else if (t.kind === "f64" || t.kind === "date") this.B.terminate(`ret double ${f64Lit(0)}`);
|
||||
else if (t.kind === "bool") this.B.terminate("ret i1 false");
|
||||
else this.B.terminate("ret ptr null");
|
||||
this.B.br(cleanup.label);
|
||||
}
|
||||
|
||||
/** The emitter contract for exceptions: after EVERY call that can throw
|
||||
@@ -2566,7 +2544,7 @@ export class LlEmitter {
|
||||
truthy(v: LlValue): string {
|
||||
const B = this.B;
|
||||
if (v.type.kind === "union") {
|
||||
// The ARM value's ToBoolean: an inline tag switch (the C emitter's
|
||||
// The ARM value's ToBoolean: an inline tag switch (the runtime ABI’s
|
||||
// per-union interned helper, emitted at the use site instead).
|
||||
const def = this.unionsById.get(v.type.unionId);
|
||||
if (!def) throw new InternalCompilerError(`llvm emitter bug: truthiness of unknown union ${v.type.unionId}`);
|
||||
@@ -2575,7 +2553,7 @@ export class LlEmitter {
|
||||
const join = B.newLabel("ut.j");
|
||||
this.unionTagSwitch(v.name, def, (arm) => {
|
||||
let valueName = "false";
|
||||
if (arm.kind === "f64") {
|
||||
if (arm.kind === "f64" || arm.kind === "procStream") {
|
||||
valueName = B.tmp();
|
||||
this.declare(`declare double @scr_union_get_f64(ptr)`);
|
||||
B.line(`${valueName} = call double @scr_union_get_f64(ptr ${v.name})`);
|
||||
@@ -2585,7 +2563,7 @@ export class LlEmitter {
|
||||
B.line(`${valueName} = call zeroext i1 @scr_union_get_bool(ptr ${v.name})`);
|
||||
} else if (arm.kind === "string") {
|
||||
valueName = this.unionPeek(v.name);
|
||||
} else if (arm.kind === "bigint") {
|
||||
} else if (arm.kind === "bigint" || arm.kind === "dyn" || arm.kind === "jsval") {
|
||||
valueName = this.unionPeek(v.name);
|
||||
}
|
||||
const truthy = this.truthyOf(arm.kind, valueName, true);
|
||||
@@ -2607,13 +2585,10 @@ export class LlEmitter {
|
||||
switch (kind) {
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
if (!unionArm) throw new LlvmUnsupportedError(`truthy:${kind}`);
|
||||
return "false";
|
||||
case "bool":
|
||||
return valueName;
|
||||
case "f64":
|
||||
case "procStream": {
|
||||
if (unionArm && kind === "procStream") throw new LlvmUnsupportedError(`truthy:union:${kind}`);
|
||||
case "f64": {
|
||||
const truthy = B.tmp();
|
||||
B.line(`${truthy} = fcmp one double ${valueName}, ${f64Lit(0)}`);
|
||||
return truthy;
|
||||
@@ -2634,26 +2609,30 @@ export class LlEmitter {
|
||||
return truthy;
|
||||
}
|
||||
case "date":
|
||||
// Process streams use a numeric descriptor internally, including zero
|
||||
// for stdin. They remain JavaScript objects in boolean contexts.
|
||||
case "procStream":
|
||||
return "true";
|
||||
case "array": case "record": case "object": case "classval": case "func":
|
||||
case "map": case "set": case "symbol": case "regex": case "promise": case "bytes":
|
||||
case "url": case "searchParams": case "stats": case "fileHandle": case "spawnRes":
|
||||
case "child": case "childStream": case "childWriter": case "generator": case "fsWatcher":
|
||||
case "cryptoHash": case "cryptoHmac": {
|
||||
case "cryptoHash": case "cryptoHmac":
|
||||
case "netSocket": case "netServer": case "httpReq": case "httpRes":
|
||||
case "httpClientReq": case "http2Session": case "http2Stream":
|
||||
case "dgramSocket": case "secureCtx": case "testCtx": {
|
||||
if (unionArm) return "true";
|
||||
const truthy = B.tmp();
|
||||
B.line(`${truthy} = icmp ne ptr ${valueName}, null`);
|
||||
return truthy;
|
||||
}
|
||||
case "dyn": {
|
||||
if (unionArm) throw new LlvmUnsupportedError(`truthy:union:${kind}`);
|
||||
this.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`);
|
||||
const truthy = B.tmp();
|
||||
B.line(`${truthy} = call zeroext i1 @scr_dyn_truthy(ptr ${valueName})`);
|
||||
return truthy;
|
||||
}
|
||||
case "jsval": {
|
||||
if (unionArm) throw new LlvmUnsupportedError(`truthy:union:${kind}`);
|
||||
this.declare(`declare i32 @scr_jsval_truthy(ptr)`);
|
||||
const raw = B.tmp();
|
||||
const truthy = B.tmp();
|
||||
@@ -2689,19 +2668,45 @@ export class LlEmitter {
|
||||
|
||||
/** Emits `switch` over a union's tag with one block per arm; each arm
|
||||
* body must TERMINATE its block (the callers branch to a join). The
|
||||
* default block is the C emitter's invalid-tag abort. */
|
||||
unionTagSwitch(uName: string, def: IrUnionDef, arm: (armType: IrType, tag: number) => void): void {
|
||||
* default block is the runtime ABI’s invalid-tag abort. Shared field
|
||||
* reads can group equivalent storage prefixes and emit one representative. */
|
||||
unionTagSwitch(uName: string, def: IrUnionDef, arm: (armType: IrType, tag: number) => void, fieldGroups?: number[][]): void {
|
||||
const B = this.B;
|
||||
const tag = this.unionTag(uName);
|
||||
const bad = B.newLabel("u.bad");
|
||||
const labels = def.arms.map(() => B.newLabel("u.a"));
|
||||
B.terminate(
|
||||
`switch i32 ${tag}, label %${bad} [ ${def.arms.map((_, i) => `i32 ${i}, label %${labels[i]}`).join(" ")} ]`,
|
||||
);
|
||||
def.arms.forEach((a, i) => {
|
||||
B.startBlock(labels[i]!);
|
||||
arm(a, i);
|
||||
});
|
||||
const labels: string[] = [];
|
||||
if (fieldGroups) {
|
||||
for (const group of fieldGroups) {
|
||||
const label = B.newLabel("u.a");
|
||||
for (const member of group) {
|
||||
if (member === undefined) throw new InternalCompilerError("llvm emitter bug: missing union field tag");
|
||||
labels[member] = label;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < def.arms.length; i++) labels.push(B.newLabel("u.a"));
|
||||
}
|
||||
if (fieldGroups?.length === 1) {
|
||||
const valid = B.tmp();
|
||||
B.line(`${valid} = icmp ult i32 ${tag}, ${def.arms.length}`);
|
||||
B.condBr(valid, labels[0]!, bad);
|
||||
} else {
|
||||
B.terminate(
|
||||
`switch i32 ${tag}, label %${bad} [ ${def.arms.map((_, i) => `i32 ${i}, label %${labels[i]}`).join(" ")} ]`,
|
||||
);
|
||||
}
|
||||
if (fieldGroups) {
|
||||
for (const group of fieldGroups) {
|
||||
const representative = group[0]!;
|
||||
B.startBlock(labels[representative]!);
|
||||
arm(def.arms[representative]!, representative);
|
||||
}
|
||||
} else {
|
||||
def.arms.forEach((a, i) => {
|
||||
B.startBlock(labels[i]!);
|
||||
arm(a, i);
|
||||
});
|
||||
}
|
||||
B.startBlock(bad);
|
||||
this.needsBadTag = true;
|
||||
B.line(`call void @sc_bad_tag()`);
|
||||
@@ -2808,6 +2813,33 @@ export class LlEmitter {
|
||||
return shape;
|
||||
}
|
||||
|
||||
/** Record prefixes with the same LLVM storage types put a field at the
|
||||
* same offset on every target. Share one field-read block per matching
|
||||
* prefix; class variants retain their own concrete layout dispatch. */
|
||||
unionFieldGroups(def: IrUnionDef, field: string): number[][] {
|
||||
const key = `${def.id}\0${field}`;
|
||||
const cached = this.unionFieldReadGroups.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
const groups = new Map<string, number[]>();
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const arm = def.arms[tag]!;
|
||||
let layout = `variant:${tag}`;
|
||||
if (arm.kind === "record") {
|
||||
const shape = this.recordShape(arm.shapeId);
|
||||
const index = shape.fields.findIndex((entry) => entry.name === field);
|
||||
if (index < 0) throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
|
||||
layout = "record:";
|
||||
for (let i = 0; i <= index; i++) layout += `${llFieldType(shape.fields[i]!.type)};`;
|
||||
}
|
||||
const group = groups.get(layout);
|
||||
if (group) group.push(tag);
|
||||
else groups.set(layout, [tag]);
|
||||
}
|
||||
const result = [...groups.values()];
|
||||
this.unionFieldReadGroups.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The field-slot pointer of a record member (rc header at index 0). */
|
||||
recordFieldPtr(objName: string, shapeId: string, field: string): { ptr: string; type: IrType } {
|
||||
const shape = this.recordShape(shapeId);
|
||||
@@ -3031,7 +3063,7 @@ export class LlEmitter {
|
||||
* async bodies are entered via their emitted spawn wrapper (which runs
|
||||
* the fiber eagerly to its first suspension and returns the promise);
|
||||
* generator bodies via theirs (which only ALLOCATES the suspended
|
||||
* fiber and returns the generator object) — CEmitter.callTargetC. */
|
||||
* fiber and returns the generator object). */
|
||||
callTarget(fnName: string): string {
|
||||
const fn = this.fnByName.get(fnName);
|
||||
if (fn?.generator !== undefined) return mangleGenSpawn(fnName);
|
||||
@@ -3136,6 +3168,7 @@ export class LlEmitter {
|
||||
B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null;
|
||||
this.frames = [];
|
||||
this.scopes = [];
|
||||
this.unwindCleanups.clear();
|
||||
this.jumpTargets = [];
|
||||
this.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
|
||||
@@ -3156,7 +3189,7 @@ export class LlEmitter {
|
||||
this.declare(`declare token @llvm.coro.save(ptr)`);
|
||||
this.declare(`declare i8 @llvm.coro.suspend(token, i1)`);
|
||||
this.declare(`declare ptr @llvm.coro.free(token, ptr)`);
|
||||
this.declare(`declare i1 @llvm.coro.end(ptr, i1, token)`);
|
||||
this.declare(`declare void @llvm.coro.end(ptr, i1, token)`);
|
||||
this.declare(`declare ptr @malloc(${this.sizeType})`);
|
||||
this.declare(`declare void @free(ptr)`);
|
||||
this.declare(`declare void @scr_wasi_coro_started(ptr)`);
|
||||
@@ -3287,8 +3320,7 @@ export class LlEmitter {
|
||||
B.line(`call void @free(ptr ${frame})`);
|
||||
B.br(coro.suspendLabel);
|
||||
B.startBlock(coro.suspendLabel);
|
||||
const ended = B.tmp();
|
||||
B.line(`${ended} = call i1 @llvm.coro.end(ptr ${coro.handle}, i1 false, token none)`);
|
||||
B.line(`call void @llvm.coro.end(ptr ${coro.handle}, i1 false, token none)`);
|
||||
const ret = this.llType(fn.returnType);
|
||||
if (ret === "void") B.terminate(`ret void`);
|
||||
else if (ret === "double") B.terminate(`ret double ${f64Lit(0)}`);
|
||||
@@ -3296,6 +3328,15 @@ export class LlEmitter {
|
||||
else B.terminate(`ret ptr null`);
|
||||
}
|
||||
|
||||
// Shared blocks have multiple source locations. Keep the location on
|
||||
// each incoming exception check instead of assigning one to the cleanup.
|
||||
B.debugLocation = null;
|
||||
for (const cleanup of this.unwindCleanups.values()) {
|
||||
B.startBlock(cleanup.label);
|
||||
this.releaseScope(cleanup.entries);
|
||||
B.terminate(cleanup.terminator);
|
||||
}
|
||||
|
||||
if (this.logArgSlots > 0) {
|
||||
B.entryAllocas.push(`%logargs = alloca [${this.logArgSlots} x %ScrLogArg]`);
|
||||
}
|
||||
@@ -3313,15 +3354,15 @@ export class LlEmitter {
|
||||
private emitStmts(stmts: IrStmt[]): void {
|
||||
for (const s of stmts) {
|
||||
// Statements after a terminator are unreachable (dead code after
|
||||
// return/break/continue) — the C emitter emits them as dead C; here
|
||||
// they are skipped so no dropped SSA definition can leak forward.
|
||||
// return/break/continue). Skip them so no dropped SSA definition
|
||||
// can leak forward.
|
||||
if (this.B.isTerminated()) return;
|
||||
this.emitStmt(s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits a block in its own lexical scope (refcounted locals released at
|
||||
* end) — CEmitter.emitBlock without the braces. `setup` runs after the
|
||||
* end). `setup` runs after the
|
||||
* scope opens, before the statements — the catch-binding hook: it may
|
||||
* emit prelude lines and register entries the scope owns (released on
|
||||
* every exit, jumps and unwinds included). */
|
||||
@@ -3921,10 +3962,7 @@ export class LlEmitter {
|
||||
// tryStack it sees truncated to its region (its releases already
|
||||
// ran; a throw inside a copy propagates OUT of the completing
|
||||
// try, past its own catch), then the function-level releases and
|
||||
// the actual ret. The C emitter routes this through per-region
|
||||
// finally copies behind gotos with the value parked in sc_pret;
|
||||
// the inline copies here are the same code at the same depths,
|
||||
// with the parked value's ownership riding a synthetic slot-based
|
||||
// the actual ret. The parked value owns a synthetic slot-backed
|
||||
// scope entry during each copy so a throwing finally releases it.
|
||||
let v: LlValue | null = null;
|
||||
if (s.value !== null) {
|
||||
@@ -4172,7 +4210,7 @@ export class LlEmitter {
|
||||
|
||||
/** JS-exact switch: lazily evaluated, arbitrary-expression case tests in
|
||||
* source order, bodies falling through in source order until a break —
|
||||
* CEmitter.emitSwitch's goto chain, block-flavored. All case bodies
|
||||
* A chain of conditional branches. All case bodies
|
||||
* share ONE scope; because dispatch can jump PAST a varDecl into a later
|
||||
* case, refcounted/boxed case-body locals are NULL-reset up front and
|
||||
* the scope-exit releases rely on NULL tolerance. */
|
||||
@@ -4253,7 +4291,7 @@ export class LlEmitter {
|
||||
/** Evaluates a condition (IR conds are bool-typed) and releases its
|
||||
* temps BEFORE the branch — safe because the result is a scalar i1, and
|
||||
* required in loop-condition blocks (their temps must not survive into
|
||||
* later blocks across the back edge). CEmitter.emitCondition. */
|
||||
* later blocks across the back edge). */
|
||||
private emitCondition(cond: IrExpr): string {
|
||||
const v = this.emitExpr(cond);
|
||||
const frame = this.currentFrame();
|
||||
@@ -4265,7 +4303,7 @@ export class LlEmitter {
|
||||
/** Evaluates `expr` in its own statement frame inside an already-open
|
||||
* branch and moves the result into `slot`: the chosen value's ownership
|
||||
* transfers, every other temp the arm allocated releases inside the
|
||||
* branch. The shared core of ternary/logical. CEmitter.emitBranchInto. */
|
||||
* branch. The shared core of ternary/logical. */
|
||||
emitBranchInto(slot: string, expr: IrExpr): void {
|
||||
this.frames.push([]);
|
||||
const v = this.emitExpr(expr);
|
||||
@@ -4376,6 +4414,36 @@ export class LlEmitter {
|
||||
return emitJsInteropExpr(this, e);
|
||||
}
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
* load. No user code may run between this read and its consumer. Locals
|
||||
* and their projections already have an owner; other expressions keep
|
||||
* their ordinary statement-frame ownership. The caller must retain any
|
||||
* reference result before evaluating another expression. */
|
||||
emitReadReceiver(e: IrExpr): LlValue {
|
||||
if (!isRefCounted(e.type)) return this.emitExpr(e);
|
||||
if (e.kind === "varRef") {
|
||||
const binding = this.binding(e.localId);
|
||||
// Capture boxes can carry TDZ and caught-value conversion semantics.
|
||||
if (binding.kind === "boxed") return this.emitExpr(e);
|
||||
if (binding.kind === "global") this.checkGlobalTdz(e.localId);
|
||||
const value = this.B.tmp();
|
||||
this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = this.emitReadReceiver(e.value);
|
||||
return { name: this.unionPeek(union.name), type: e.type };
|
||||
}
|
||||
if (e.kind === "recordGet" || e.kind === "fieldGet") {
|
||||
const receiver = this.emitReadReceiver(e.obj);
|
||||
const { ptr, type } = e.kind === "recordGet"
|
||||
? this.recordFieldPtr(receiver.name, e.shapeId, e.field)
|
||||
: this.classFieldPtr(receiver.name, e.className, e.field);
|
||||
return { name: this.loadField(ptr, type), type: e.type };
|
||||
}
|
||||
return this.emitExpr(e);
|
||||
}
|
||||
|
||||
emitExpr(e: IrExpr): LlValue {
|
||||
return emitExpr(this, e);
|
||||
}
|
||||
|
||||
@@ -201,8 +201,7 @@ export function emitSerializationExpr(host: LlvmEmitterContext, e: ExprOf<"jsonS
|
||||
const v = host.emitExpr(e.value);
|
||||
let compact: { name: string; type: IrType };
|
||||
if (e.value.type.kind === "dyn") {
|
||||
// A dyn root: the runtime's dyn walker (scr_dyn_format_j — the
|
||||
// C backend's dispatch exactly): number/string/bool/null/array/
|
||||
// A dyn root: the runtime's dyn walker (scr_dyn_format_j): number/string/bool/null/array/
|
||||
// object exact, dropped members omitted, and a dropped ROOT
|
||||
// becomes the TEXT "undefined" (JSON.stringify(undefined) is
|
||||
// the undefined value; printing it spells the word — Node's
|
||||
@@ -228,7 +227,7 @@ export function emitSerializationExpr(host: LlvmEmitterContext, e: ExprOf<"jsonS
|
||||
// A cycle-capable root can throw the circular-structure
|
||||
// TypeError mid-walk: finish still runs (frees the buffer, the
|
||||
// partial string joins the frame and releases on unwind), then
|
||||
// the pending check unwinds — the C emitter's contract exactly.
|
||||
// the pending check unwinds — the runtime ABI’s contract exactly.
|
||||
if (traceAdapter(host.shapeHost, e.value.type) !== null) host.emitPendingCheck();
|
||||
}
|
||||
// A pretty-print form (`stringify(v, null, 2)`): the frontend
|
||||
|
||||
@@ -630,10 +630,11 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
}
|
||||
|
||||
export function emitterFixedAdapter(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
|
||||
// SCR_EE_FIXED_MAX (scr_runtime.h): the registry's audited arity
|
||||
// ceiling — refuse past it rather than guess.
|
||||
if (cbT.params.length > 4) throw new LlvmUnsupportedError(`emitterListenerArity:${cbT.params.length}`);
|
||||
const shim = `scr_ee_inv_fixed${cbT.params.length}`;
|
||||
// Short tuples use direct fixed signatures. Longer listeners read an
|
||||
// opaque runtime cursor so no platform va_list layout leaks into LLVM.
|
||||
const cursor = cbT.params.length > 4;
|
||||
const shim = cursor ? "scr_ee_inv_args" : `scr_ee_inv_fixed${cbT.params.length}`;
|
||||
if (cursor) host.declare("declare ptr @scr_ee_arg_next(ptr)");
|
||||
// Only the shim's ADDRESS rides the .ll (the runtime calls it through
|
||||
// its real ScrEeInvoke type); the (ptr, ptr) spelling is layout-free.
|
||||
host.declare(`declare void @${shim}(ptr, ptr)`);
|
||||
@@ -644,7 +645,7 @@ export function emitterFixedAdapter(host: LlvmEmitterContext, cbT: IrType & { ki
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
const params = cbT.params.map((_, i) => `ptr %a${i}`).join(", ");
|
||||
const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", ");
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb${params ? ", " + params : ""}) ${FN_ATTRS} { ; emitter listener adapter ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
@@ -654,6 +655,7 @@ export function emitterFixedAdapter(host: LlvmEmitterContext, cbT: IrType & { ki
|
||||
];
|
||||
const passed: string[] = ["ptr %orig"];
|
||||
cbT.params.forEach((p, i) => {
|
||||
if (cursor) d.push(` %a${i} = call ptr @scr_ee_arg_next(ptr %args)`);
|
||||
const ty = host.llType(p);
|
||||
if (ty === "double") {
|
||||
d.push(` %d${i} = load double, ptr %a${i}`);
|
||||
|
||||
@@ -593,7 +593,7 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
}
|
||||
case "promiseVoidWiden": {
|
||||
// One ScrPromise* either way — ownership transfers, type-only
|
||||
// (the C emitter's rule).
|
||||
//.
|
||||
const v = host.emitExpr(e.value);
|
||||
host.moveTemp(v);
|
||||
return host.own({ name: v.name, type: e.type });
|
||||
|
||||
@@ -57,7 +57,7 @@ export function emitControlExpr(host: LlvmEmitterContext, e: ExprOf<"dynDestrChe
|
||||
// JS value semantics: the result is the deciding operand itself.
|
||||
// Left evaluates once, ownership moves into the result slot; when
|
||||
// the branch takes the right operand the stale left releases first
|
||||
// and the right runs in its own frame — CEmitter's dance.
|
||||
// and the right runs in its own frame.
|
||||
const ty = host.llType(e.type);
|
||||
const l = host.emitExpr(e.left);
|
||||
host.moveTemp(l);
|
||||
|
||||
@@ -62,7 +62,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
|
||||
const boxed = B.tmp();
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, v.type)}(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, v.type)}(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
return host.own({ name: boxed, type: e.type });
|
||||
}
|
||||
@@ -239,9 +239,8 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
case "unionNarrow": {
|
||||
// Tag-UNCHECKED payload extraction: the frontend emits this only
|
||||
// where tsc's control-flow narrowing proved the tag. Ref payloads
|
||||
// come out +1; the union temp itself releases with this
|
||||
// statement's frame as usual.
|
||||
const u = host.emitExpr(e.value);
|
||||
// come out +1. The receiver is consumed before any later expression.
|
||||
const u = host.emitReadReceiver(e.value);
|
||||
const arm = e.type;
|
||||
if (isUnitType(arm)) throw new InternalCompilerError(`llvm emitter bug: unionNarrow to unit arm ${arm.kind}`);
|
||||
const v = host.unionExtract(u.name, arm);
|
||||
@@ -251,7 +250,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
// Shared-field read `r.kind`: switch on the runtime tag and read
|
||||
// the (same-typed) field from the concretely-typed payload.
|
||||
// Ref-counted results come out retained (+1), owned by this frame.
|
||||
const u = host.emitExpr(e.value);
|
||||
const u = host.emitReadReceiver(e.value);
|
||||
const def = host.unionsById.get(e.unionId);
|
||||
if (!def) throw new InternalCompilerError(`llvm emitter bug: unionDisc of unknown union ${e.unionId}`);
|
||||
const ty = host.llType(e.type);
|
||||
@@ -271,7 +270,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
const value = isRefCounted(e.type) ? host.retainValue(v, e.type) : v;
|
||||
B.line(`store ${ty} ${value}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
});
|
||||
}, host.unionFieldGroups(def, e.field));
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
@@ -400,7 +399,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
}
|
||||
case "unionIsTag": {
|
||||
// A pure tag compare — the box is borrowed, no payload is touched.
|
||||
const u = host.emitExpr(e.value);
|
||||
const u = host.emitReadReceiver(e.value);
|
||||
const tag = host.unionTag(u.name);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = icmp ${e.negated ? "ne" : "eq"} i32 ${tag}, ${e.tag}`);
|
||||
@@ -558,7 +557,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
B.line(`${o} = or i1 ${acc}, ${c}`);
|
||||
return o;
|
||||
};
|
||||
if (e.test === "nullish") {
|
||||
if (e.test === "promise") {
|
||||
test = oneOf([DYN_KIND.PROMISE]);
|
||||
} else if (e.test === "nullish") {
|
||||
test = oneOf([DYN_KIND.UNDEF, DYN_KIND.NULL]);
|
||||
} else if (e.test === "buffer") {
|
||||
const bytes = oneOf([DYN_KIND.BYTES]);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function emitJsMarshal(host: LlvmEmitterContext, e: IrExpr & { kind: "jsM
|
||||
// data kinds only — boxed functions/handles/promises throw the
|
||||
// catchable TypeError in the runtime, and a wrapped island value
|
||||
// unwraps to the SAME engine value (the identity round trip).
|
||||
// The C emitter's rule, mirrored.
|
||||
// Transfer ownership to the island runtime.
|
||||
return simple("scr_jsval_from_dyn", "ptr", true);
|
||||
case "bytes":
|
||||
// A typed array crossing IN: a COPY (the boundary's copy stance).
|
||||
@@ -41,7 +41,7 @@ export function emitJsMarshal(host: LlvmEmitterContext, e: IrExpr & { kind: "jsM
|
||||
// A STATIC promise crossing IN: a real engine thenable settled
|
||||
// when the scriptc promise settles (the async-callback return
|
||||
// bridge). from_promise takes ownership of a +1 — retain past
|
||||
// the borrowed frame temp. The C emitter's rule, mirrored.
|
||||
// the borrowed frame temp. Transfer ownership to the island runtime.
|
||||
const tag = islandPromisePayloadTag(e.value.type.inner);
|
||||
if (!tag) throw new InternalCompilerError("llvm emitter bug: jsMarshal of a promise outside the bridge payload domain");
|
||||
const tagN = { void: 0, f64: 1, bool: 2, string: 3, jsval: 4, jsvalArr: 5 }[tag];
|
||||
@@ -64,7 +64,7 @@ export function emitJsMarshal(host: LlvmEmitterContext, e: IrExpr & { kind: "jsM
|
||||
? host.islandAdapter(fn.params.length, fn.ret.kind as "void" | "jsval" | "f64" | "bool" | "string")
|
||||
: host.islandTypedAdapter(fn);
|
||||
host.declare(`declare ptr @scr_jsval_from_closure(ptr, i32, ptr)`);
|
||||
// ISLAND-REST closures encode a NEGATIVE arity (the C emitter's
|
||||
// ISLAND-REST closures encode a NEGATIVE arity (the runtime ABI’s
|
||||
// rule): the wrapper hands the trailing slot the engine array of
|
||||
// the surplus arguments.
|
||||
const arity = fn.rest === true && fn.restAbi === "jsval" ? -fn.params.length : fn.params.length;
|
||||
@@ -291,7 +291,7 @@ export function emitJsOp(host: LlvmEmitterContext, e: IrExpr & { kind: "jsOp" })
|
||||
});
|
||||
}
|
||||
case "defineGetter": {
|
||||
// Getter completion for an island literal (the C emitter's
|
||||
// Getter completion for an island literal (the runtime ABI’s
|
||||
// scr_jsval_define_getter shape): defines key a(1) on obj a(0)
|
||||
// as an engine getter invoking a(2); answers the object (+1).
|
||||
host.declare(`declare ptr @scr_jsval_define_getter(ptr, ptr, ptr)`);
|
||||
@@ -309,7 +309,7 @@ export function emitJsOp(host: LlvmEmitterContext, e: IrExpr & { kind: "jsOp" })
|
||||
case "instanceOf": {
|
||||
// JS_IsInstanceOf through the engine: 1 true, 0 false, -1 threw
|
||||
// (Symbol.hasInstance can raise) — the fallible pattern, result
|
||||
// narrowed to bool by comparing against 1 (the C emitter's shape).
|
||||
// narrowed to bool by comparing against 1.
|
||||
host.declare(`declare i32 @scr_jsval_instance_of(ptr, ptr)`);
|
||||
return fallible(() => {
|
||||
const r = B.tmp();
|
||||
|
||||
@@ -69,6 +69,12 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
case "bin": {
|
||||
// JavaScript folds literal 0 / 0 to its positive NaN constant.
|
||||
// An unoptimized x86 division produces a negative NaN instead,
|
||||
// whose sign is observable through Buffer and typed-array writes.
|
||||
if (e.op === "/" && e.left.kind === "numLit" && e.right.kind === "numLit" && e.left.value === 0 && e.right.value === 0) {
|
||||
return { name: f64Lit(NaN), type: e.type };
|
||||
}
|
||||
const l = host.emitExpr(e.left);
|
||||
const r = host.emitExpr(e.right);
|
||||
const t = B.tmp();
|
||||
@@ -197,7 +203,7 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
|
||||
case "assignExpr": {
|
||||
// `x = e` in expression position: the binding takes its OWN
|
||||
// reference (retain for ref kinds), the temp stays the yielded
|
||||
// value — CEmitter's order exactly (release old, store retained).
|
||||
// value — release the old value before storing the retained value.
|
||||
const concat = e.value;
|
||||
const suffix = matchStringSelfConcat(e.localId, concat);
|
||||
if (suffix && concat.kind === "strConcat") {
|
||||
@@ -390,7 +396,7 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" |
|
||||
// One immortal static per (pattern, flags) pair; the +1 retain is
|
||||
// a no-op on immortals but keeps the owned-temps discipline
|
||||
// uniform. Pattern/flags strings intern NOW (the literal table is
|
||||
// still open — the C emitter's regex-literal discipline).
|
||||
// still open — the runtime ABI’s regex-literal discipline).
|
||||
const key = `${e.flags}/${e.pattern}`;
|
||||
let re = host.regexInstances.get(key);
|
||||
if (!re) {
|
||||
@@ -572,14 +578,14 @@ export function emitRecordExpr(host: LlvmEmitterContext, e: ExprOf<"fieldGet" |
|
||||
const B = host.B;
|
||||
switch (e.kind) {
|
||||
case "fieldGet": {
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
const { ptr, type } = host.classFieldPtr(obj.name, e.className, e.field);
|
||||
const v = host.loadField(ptr, type);
|
||||
if (isRefCounted(e.type)) return host.own({ name: host.retainValue(v, e.type), type: e.type });
|
||||
return { name: v, type: e.type };
|
||||
}
|
||||
case "recordGet": {
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
const { ptr, type } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
|
||||
const v = host.loadField(ptr, type);
|
||||
if (isRefCounted(e.type)) return host.own({ name: host.retainValue(v, e.type), type: e.type });
|
||||
|
||||
@@ -345,7 +345,7 @@ export function keyedRecordReadInto(host: LlvmEmitterContext,
|
||||
return;
|
||||
}
|
||||
// A direct result cannot represent a miss. This is unreachable for
|
||||
// programs whose behavior matches Node, but keep the C backend's trap.
|
||||
// programs whose behavior matches Node, but preserve the missing-key trap.
|
||||
host.needsBadKey = true;
|
||||
B.line(`call void @sc_bad_key()`);
|
||||
B.terminate(`unreachable`);
|
||||
|
||||
@@ -339,7 +339,7 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
|
||||
);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
});
|
||||
@@ -438,7 +438,7 @@ function nestedTypedRefUnionAdapter(
|
||||
B.line(`${payloadPtr} = getelementptr inbounds %ScrUnion, ptr %u, i64 0, i32 5`);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
} else if (arm.kind === "undefinedT") {
|
||||
@@ -525,7 +525,7 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
|
||||
const key = typeKey(t);
|
||||
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, t)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
|
||||
);
|
||||
return boxed;
|
||||
}
|
||||
@@ -800,7 +800,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
|
||||
);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadPtr}`);
|
||||
B.line(
|
||||
`${armBoxed} = call ptr ${typedRefConstructor(host, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${armSnapshot}, ptr ${armCommit})`,
|
||||
`${armBoxed} = call ptr ${typedRefConstructor(host.shapeHost, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${armSnapshot}, ptr ${armCommit})`,
|
||||
);
|
||||
B.line(`store ptr ${armBoxed}, ptr ${boxedSlot}`);
|
||||
B.br(join);
|
||||
@@ -848,7 +848,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
|
||||
}
|
||||
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, elem)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, elem)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
|
||||
);
|
||||
} else {
|
||||
boxed = B.tmp();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Argument marshalling at the runtime C ABI boundary. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { MAY_THROW_LIB_FNS, type IrType } from "../../ir/ir.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { vAdapters } from "./shapes.js";
|
||||
|
||||
export interface AbiArgument { type: string; name: string }
|
||||
export const ptr = (name = "null"): AbiArgument => ({ type: "ptr", name });
|
||||
export function abiValue(host: LlvmEmitterContext, value: LlValue): AbiArgument {
|
||||
return { type: host.llType(value.type), name: value.name };
|
||||
}
|
||||
const parameterType = (type: string): string => type === "i1" ? "i1 zeroext" : type;
|
||||
|
||||
export function callRuntime(host: LlvmEmitterContext, symbol: string, resultType: string, args: AbiArgument[]): string {
|
||||
const result = resultType === "i1" ? "zeroext i1" : resultType;
|
||||
host.declare(`declare ${result} @${symbol}(${args.map((arg) => parameterType(arg.type)).join(", ")})`);
|
||||
const name = resultType === "void" ? "" : host.B.tmp();
|
||||
host.B.line(`${name === "" ? "" : name + " = "}call ${result} @${symbol}(${args.map((arg) => `${parameterType(arg.type)} ${arg.name}`).join(", ")})`);
|
||||
return name;
|
||||
}
|
||||
|
||||
export function finishRuntimeCall(host: LlvmEmitterContext, e: LibCallExpr, symbol: string, args: AbiArgument[]): LlValue {
|
||||
const name = callRuntime(host, symbol, host.llType(e.type), args);
|
||||
const out = e.type.kind === "void" ? { name, type: e.type } : host.own({ name, type: e.type });
|
||||
if (MAY_THROW_LIB_FNS.has(e.fn)) host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Strings inline their bytes; Buffer views carry a separate data pointer. */
|
||||
export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
|
||||
const B = host.B;
|
||||
const lengthPtr = B.tmp(), length = B.tmp(), data = B.tmp();
|
||||
B.line(`${lengthPtr} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(8, 4)}`);
|
||||
B.line(`${length} = load ${host.sizeType}, ptr ${lengthPtr}`);
|
||||
if (value.type.kind === "string") {
|
||||
B.line(`${data} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(24, 12)}`);
|
||||
} else if (value.type.kind === "bytes" && value.type.elem === "u8") {
|
||||
const dataPtr = B.tmp();
|
||||
B.line(`${dataPtr} = getelementptr inbounds i8, ptr ${value.name}, i64 ${host.abiOffset(24, 12)}`);
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}`);
|
||||
} else throw new InternalCompilerError("runtime byte argument must be a string or Buffer");
|
||||
return [ptr(data), { type: host.sizeType, name: length }];
|
||||
}
|
||||
|
||||
export function callbackType(value: LlValue): IrType & { kind: "func" } {
|
||||
if (value.type.kind !== "func") throw new InternalCompilerError("runtime callback must be a function");
|
||||
return value.type;
|
||||
}
|
||||
|
||||
export function callbackAdapter(host: LlvmEmitterContext, value: LlValue, symbol: string, params: string[]): AbiArgument[] {
|
||||
host.moveTemp(value);
|
||||
host.declare(`declare void @${symbol}(${params.join(", ")})`);
|
||||
return [ptr(value.name), ptr(`@${symbol}`)];
|
||||
}
|
||||
|
||||
export function requestHandler(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
|
||||
const arity = callbackType(value).params.length;
|
||||
return callbackAdapter(host, value, `scr_http_handler_thunk${Math.min(arity, 2)}`, ["ptr", "ptr", "ptr"]);
|
||||
}
|
||||
|
||||
/** Wrap an owned nullable runtime reference in the program's tagged union. */
|
||||
export function nullableReference(host: LlvmEmitterContext, type: IrType, value: string, presentKind: IrType["kind"], absentKind: "nullT" | "undefinedT"): LlValue {
|
||||
if (type.kind !== "union") throw new InternalCompilerError("nullable runtime result must be a union");
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const presentTag = arms.findIndex((arm) => arm.kind === presentKind);
|
||||
const absentTag = arms.findIndex((arm) => arm.kind === absentKind);
|
||||
if (presentTag < 0 || absentTag < 0) throw new InternalCompilerError("nullable runtime result lacks its union arms");
|
||||
const adapters = vAdapters(host.shapeHost, arms[presentTag]!);
|
||||
const B = host.B, yes = B.newLabel("runtime.present"), no = B.newLabel("runtime.absent"), done = B.newLabel("runtime.join");
|
||||
const exists = B.tmp();
|
||||
B.line(`${exists} = icmp ne ptr ${value}, null`);
|
||||
B.condBr(exists, yes, no);
|
||||
B.startBlock(yes);
|
||||
const wrapped = callRuntime(host, "scr_union_new_ref", "ptr", [
|
||||
{ type: "i32", name: String(presentTag) }, ptr(value), ptr(adapters.retain), ptr(adapters.release), ptr(),
|
||||
]);
|
||||
B.br(done);
|
||||
B.startBlock(no);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${wrapped}, %${yes} ], [ ${host.unitInstanceRef(type.unionId, absentTag)}, %${no} ]`);
|
||||
return host.own({ name: result, type });
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { typeKey } from "../../ir/ir.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
|
||||
/** CONNECT transfers owned request/socket/response values to the listener. */
|
||||
function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string {
|
||||
const type = callbackType(cb), union = type.params[1];
|
||||
if (union?.kind !== "union") throw new InternalCompilerError("CONNECT adapter requires a union slot");
|
||||
const kind = h2 ? "httpRes" : "netSocket";
|
||||
const tag = host.unionsById.get(union.unionId)?.arms.findIndex((arm) => arm.kind === kind) ?? -1;
|
||||
if (tag < 0) throw new InternalCompilerError(`CONNECT union lacks its ${kind} arm`);
|
||||
const key = `connect:${h2}:${typeKey(type)}`;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_connect_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
|
||||
const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v";
|
||||
host.declare(`declare ptr @${retain}(ptr)`);
|
||||
host.declare(`declare void @${release}(ptr)`);
|
||||
host.declare("declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)");
|
||||
const third = !h2 && type.params.length === 3;
|
||||
if (!h2 && !third) host.declare("declare void @scr_bytes_release(ptr)");
|
||||
host.resolveThunkDefs.push(`define internal void @${symbol}(ptr %cb, ptr %request, ptr %value${h2 ? "" : ", ptr %head"}) ${FN_ATTRS} {`, "entry:", ...(!h2 && !third ? [" call void @scr_bytes_release(ptr %head)"] : []), ` %union = call ptr @scr_union_new_ref(i32 ${tag}, ptr %value, ptr @${retain}, ptr @${release}, ptr null)`, " %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1", " %fn = load ptr, ptr %fnp", ` call void %fn(ptr %cb, ptr %request, ptr %union${third ? ", ptr %head" : ""})`, " ret void", "}", "");
|
||||
return symbol;
|
||||
}
|
||||
|
||||
export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)), a = args.map((arg) => abiValue(host, arg));
|
||||
const cb = args[1]!, type = callbackType(cb), arity = type.params.length, second = type.params[1];
|
||||
host.moveTemp(cb);
|
||||
let h1: string, h2 = "null";
|
||||
if (second?.kind === "union") {
|
||||
h1 = "@" + connectAdapter(host, cb, false);
|
||||
if (host.unionsById.get(second.unionId)?.arms.some((arm) => arm.kind === "httpRes")) h2 = "@" + connectAdapter(host, cb, true);
|
||||
} else {
|
||||
h1 = `@scr_http_upgrade_thunk${Math.min(arity, 3)}`;
|
||||
host.declare(`declare void ${h1}(ptr, ptr, ptr, ptr)`);
|
||||
}
|
||||
if (arity <= 1) {
|
||||
h2 = `@scr_http_handler_thunk${arity}`;
|
||||
host.declare(`declare void ${h2}(ptr, ptr, ptr)`);
|
||||
}
|
||||
return finishRuntimeCall(host, e, "scr_http_server_on_connect", [a[0]!, a[1]!, ptr(h1), ptr(h2), a[2]!]);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { STRING, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callRuntime, callbackAdapter, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
|
||||
function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
if (type.kind !== "record") throw new InternalCompilerError("datagram remote info must be a record");
|
||||
const key = "dgram:" + type.shapeId;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dgram_message_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
const shape = host.recordsById.get(type.shapeId);
|
||||
if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape");
|
||||
const lines: string[] = [];
|
||||
for (const field of ["address", "family", "port", "size"]) {
|
||||
const index = shape.fields.findIndex((item) => item.name === field);
|
||||
if (index < 0) throw new InternalCompilerError(`missing remote info field ${field}`);
|
||||
const isString = field === "address" || field === "family";
|
||||
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
|
||||
lines.push(` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`, ` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`);
|
||||
}
|
||||
host.declare("declare ptr @scr_str_retain_v(ptr)");
|
||||
host.declare("declare ptr @scr_bytes_retain_v(ptr)");
|
||||
host.resolveThunkDefs.push(`define internal void @${symbol}(ptr %cb, ptr %message, ptr %address, ptr %family, double %port, double %size) ${FN_ATTRS} {`, "entry:", ` %record = call ptr @${mangleRecordNew(type.shapeId)}()`, ...lines, " %owned = call ptr @scr_bytes_retain_v(ptr %message)", " %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1", " %fn = load ptr, ptr %fnp", " call void %fn(ptr %cb, ptr %owned, ptr %record)", " ret void", "}", "");
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
|
||||
const type = callbackType(cb), arity = type.params.length;
|
||||
if (arity === 0) return "scr_dns_thunk0";
|
||||
const error = type.params[0]!;
|
||||
if (error.kind !== "union") throw new InternalCompilerError("DNS callback error must be a union");
|
||||
const arms = host.unionsById.get(error.unionId)?.arms ?? [];
|
||||
const errorTag = arms.findIndex((arm) => arm.kind === "object"), nullTag = arms.findIndex((arm) => arm.kind === "nullT");
|
||||
if (errorTag < 0 || nullTag < 0) throw new InternalCompilerError("DNS callback error union lacks its arms");
|
||||
const key = `dns:${error.unionId}:${arity}`;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dns_lookup_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.declare("declare ptr @scr_error_new(i32, ptr)");
|
||||
host.declare("declare ptr @scr_error_retain_v(ptr)");
|
||||
host.declare("declare void @scr_error_release_v(ptr)");
|
||||
host.declare("declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)");
|
||||
host.declare("declare ptr @scr_str_retain_v(ptr)");
|
||||
host.resolveThunkDefs.push(`define internal void @${symbol}(ptr %cb, ptr %message, ptr %address, double %family) ${FN_ATTRS} {`, "entry:", " %failed = icmp ne ptr %message, null", " br i1 %failed, label %error, label %success", "error:", " %err.value = call ptr @scr_error_new(i32 0, ptr %message)", ` %boxed = call ptr @scr_union_new_ref(i32 ${errorTag}, ptr %err.value, ptr @scr_error_retain_v, ptr @scr_error_release_v, ptr null)`, " br label %join", "success:", " br label %join", "join:", ` %result = phi ptr [ %boxed, %error ], [ ${host.unitInstanceRef(error.unionId, nullTag)}, %success ]`, ...(arity >= 2 ? [" %owned.address = call ptr @scr_str_retain_v(ptr %address)"] : []), " %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1", " %fn = load ptr, ptr %fnp", ` call void %fn(ptr %cb, ptr %result${arity >= 2 ? ", ptr %owned.address" : ""}${arity >= 3 ? ", double %family" : ""})`, " ret void", "}", "");
|
||||
return symbol;
|
||||
}
|
||||
|
||||
export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)), a = args.map((arg) => abiValue(host, arg));
|
||||
host.usesTimers = true;
|
||||
const direct: Record<string, string> = {
|
||||
"dgram.createSocket": "scr_dgram_create", "dgram.sendStr": "scr_dgram_send_str",
|
||||
"dgram.sendBytes": "scr_dgram_send_bytes", "dgram.sendChk": "scr_dgram_send_chk",
|
||||
"dgram.ref": "scr_dgram_ref", "dgram.unref": "scr_dgram_unref",
|
||||
};
|
||||
const symbol = direct[e.fn];
|
||||
if (symbol !== undefined) return finishRuntimeCall(host, e, symbol, a);
|
||||
switch (e.fn) {
|
||||
case "dgram.bind": case "dgram.bindCb": case "dgram.connect": case "dgram.connectCb":
|
||||
if (args[3] !== undefined) host.moveTemp(args[3]);
|
||||
return finishRuntimeCall(host, e, e.fn.startsWith("dgram.bind") ? "scr_dgram_bind" : "scr_dgram_connect", [...a.slice(0, 3), a[3] ?? ptr()]);
|
||||
case "dgram.close": case "dgram.closeCb":
|
||||
if (args[1] !== undefined) host.moveTemp(args[1]);
|
||||
return finishRuntimeCall(host, e, "scr_dgram_close", [a[0]!, a[1] ?? ptr()]);
|
||||
case "dgram.address": {
|
||||
const type = e.type;
|
||||
if (type.kind !== "record") throw new InternalCompilerError("datagram address must be a record");
|
||||
const ip = host.own({ name: callRuntime(host, "scr_dgram_addr_ip", "ptr", a), type: STRING });
|
||||
host.emitPendingCheck();
|
||||
const record = host.B.tmp();
|
||||
host.B.line(`${record} = call ptr @${mangleRecordNew(type.shapeId)}()`);
|
||||
const out = host.own({ name: record, type });
|
||||
host.moveTemp(ip);
|
||||
const shape = host.recordsById.get(type.shapeId);
|
||||
if (shape === undefined) throw new InternalCompilerError("missing datagram address shape");
|
||||
for (const field of ["address", "family", "port"]) {
|
||||
const index = shape.fields.findIndex((item) => item.name === field);
|
||||
if (index < 0) throw new InternalCompilerError(`missing datagram address field ${field}`);
|
||||
const ty = field === "port" ? "double" : "ptr";
|
||||
const value = field === "address" ? ip.name : callRuntime(host, field === "family" ? "scr_dgram_addr_family" : "scr_dgram_addr_port", ty, a);
|
||||
const slot = host.B.tmp();
|
||||
host.B.line(`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`);
|
||||
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case "dgram.onMessage": {
|
||||
const cb = args[1]!, type = callbackType(cb);
|
||||
const adapter = type.params.length === 0 ? "scr_dgram_msg_thunk0" : type.params.length === 1 ? "scr_dgram_msg_thunk1" : messageAdapter(host, type.params[1]!);
|
||||
// Program-specific adapters are definitions, so do not redeclare them.
|
||||
host.moveTemp(cb);
|
||||
if (type.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr, ptr, double, double)`);
|
||||
return finishRuntimeCall(host, e, "scr_dgram_on_message", [a[0]!, a[1]!, ptr("@" + adapter), a[2]!]);
|
||||
}
|
||||
case "dgram.onError": {
|
||||
const cb = args[1]!;
|
||||
return finishRuntimeCall(host, e, "scr_dgram_on_error", [a[0]!, ...callbackAdapter(host, cb, callbackType(cb).params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error", ["ptr", "ptr"]), a[2]!]);
|
||||
}
|
||||
case "dgram.onListening": case "dgram.onClose": case "dgram.onConnect":
|
||||
host.moveTemp(args[1]!);
|
||||
return finishRuntimeCall(host, e, e.fn === "dgram.onListening" ? "scr_dgram_on_listening" : e.fn === "dgram.onClose" ? "scr_dgram_on_close" : "scr_dgram_on_connect", a);
|
||||
case "dns.lookup": {
|
||||
const cb = args[2]!, symbol = lookupAdapter(host, cb);
|
||||
host.moveTemp(cb);
|
||||
if (callbackType(cb).params.length === 0) host.declare("declare void @scr_dns_thunk0(ptr, ptr, ptr, double)");
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup", [...a, ptr("@" + symbol)]);
|
||||
}
|
||||
default: throw new InternalCompilerError(`unhandled datagram call ${e.fn}`);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { emitTlsLibCall } from "./lib-tls.js";
|
||||
import { emitHttp2LibCall } from "./lib-http2.js";
|
||||
import { emitDatagramLibCall } from "./lib-datagram.js";
|
||||
import { emitTestLibCall } from "./lib-test.js";
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/ir.js";
|
||||
@@ -252,9 +256,10 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "passthrough":
|
||||
case "sc":
|
||||
return host.emitStreamLibCall(e);
|
||||
case "net":
|
||||
case "dgram":
|
||||
case "dns":
|
||||
return emitDatagramLibCall(host, e);
|
||||
case "net":
|
||||
case "http":
|
||||
case "https":
|
||||
return host.emitNetworkHttpLibCall(e);
|
||||
@@ -273,11 +278,11 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "crypto":
|
||||
case "buffer":
|
||||
case "bytes":
|
||||
case "test":
|
||||
case "tls":
|
||||
case "tlsca":
|
||||
case "http2":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test": return emitTestLibCall(host, e);
|
||||
case "tls": return emitTlsLibCall(host, e);
|
||||
case "http2": return emitHttp2LibCall(host, e);
|
||||
default: {
|
||||
const _exhaustive: never = prefix;
|
||||
void _exhaustive;
|
||||
|
||||
@@ -391,8 +391,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (e.fn === "qs.parse") {
|
||||
// The ParsedUrlQuery dictionary: a fresh pure-index-signature
|
||||
// record whose overflow map the runtime scan fills
|
||||
// (scr_qs_parse_into groups repeats into string[] buckets) — the
|
||||
// C emitter's shape exactly. The frontend verified the structure;
|
||||
// (scr_qs_parse_into groups repeats into string[] buckets).
|
||||
// The frontend verified the structure;
|
||||
// lookups here only guard emitter bugs. Args: qs, sep, eq, maxKeys.
|
||||
if (e.type.kind !== "record") throw new InternalCompilerError("llvm emitter bug: qs.parse result is not a record");
|
||||
const dictShape = host.recordsById.get(e.type.shapeId);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import { typeKey } from "../../ir/ir.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callbackAdapter, callbackType, finishRuntimeCall, ptr, rawBytes, requestHandler } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
|
||||
/** The runtime owns the answer closure; its arguments arrive owned from the caller. */
|
||||
function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
if (type.kind !== "func") throw new InternalCompilerError("SNI answer must be a function");
|
||||
const key = "sni:" + typeKey(type);
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_sni_answer_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
const params = ["ptr %self"], body: string[] = [];
|
||||
let hasError = "false", context = "null";
|
||||
host.declare("declare void @scr_union_release(ptr)");
|
||||
host.declare("declare void @scr_tls_sni_answer(ptr, i1 zeroext, ptr)");
|
||||
if (type.params.length >= 1) {
|
||||
const error = type.params[0]!;
|
||||
const nullTag = error.kind === "union" ? host.unionsById.get(error.unionId)?.arms.findIndex((arm) => arm.kind === "nullT") ?? -1 : -1;
|
||||
if (nullTag < 0) throw new InternalCompilerError("SNI error argument lacks its null arm");
|
||||
params.push("ptr %error");
|
||||
body.push(" %error.tag.ptr = getelementptr inbounds %ScrUnion, ptr %error, i64 0, i32 1", " %error.tag = load i32, ptr %error.tag.ptr", ` %has.error = icmp ne i32 %error.tag, ${nullTag}`, " call void @scr_union_release(ptr %error)");
|
||||
hasError = "%has.error";
|
||||
}
|
||||
if (type.params.length >= 2) {
|
||||
const ctx = type.params[1]!;
|
||||
const tag = ctx.kind === "union" ? host.unionsById.get(ctx.unionId)?.arms.findIndex((arm) => arm.kind === "secureCtx") ?? -1 : -1;
|
||||
if (tag < 0) throw new InternalCompilerError("SNI context argument lacks its SecureContext arm");
|
||||
params.push("ptr %context");
|
||||
host.declare("declare ptr @scr_secure_ctx_retain_v(ptr)");
|
||||
body.push(" %context.tag.ptr = getelementptr inbounds %ScrUnion, ptr %context, i64 0, i32 1", " %context.tag = load i32, ptr %context.tag.ptr", ` %has.context = icmp eq i32 %context.tag, ${tag}`, " br i1 %has.context, label %present, label %absent", "present:", " %context.ptr = getelementptr inbounds %ScrUnion, ptr %context, i64 0, i32 5", " %context.value = load ptr, ptr %context.ptr", " %retained = call ptr @scr_secure_ctx_retain_v(ptr %context.value)", " br label %join", "absent:", " br label %join", "join:", " %ctx = phi ptr [ %retained, %present ], [ null, %absent ]", " call void @scr_union_release(ptr %context)");
|
||||
context = "%ctx";
|
||||
}
|
||||
host.resolveThunkDefs.push(`define internal void @${symbol}(${params.join(", ")}) ${FN_ATTRS} {`, "entry:", ...body, ` call void @scr_tls_sni_answer(ptr %self, i1 zeroext ${hasError}, ptr ${context})`, " ret void", "}", "");
|
||||
return symbol;
|
||||
}
|
||||
|
||||
export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)), a = args.map((arg) => abiValue(host, arg));
|
||||
host.usesTimers = true;
|
||||
const direct: Record<string, string> = {
|
||||
"http2.createServer": "scr_http2_create_server",
|
||||
"http2.sessionRequest": "scr_http2_session_request",
|
||||
"http2.sessionDestroy": "scr_http2_session_destroy",
|
||||
"http2.sessionSettingsDynCb": "scr_http2_session_settings_dyncb",
|
||||
"http2.sessionOnSettingsDyn": "scr_http2_session_on_settings_dyn",
|
||||
"http2.sessionSettingsGet": "scr_http2_session_settings_get",
|
||||
"http2.sessionPendingSettingsAck": "scr_http2_session_pending_settings_ack",
|
||||
"http2.getDefaultSettings": "scr_http2_get_default_settings",
|
||||
"http2.sessionClosed": "scr_http2_session_closed",
|
||||
"http2.sessionDestroyed": "scr_http2_session_destroyed",
|
||||
"http2.sessionEncrypted": "scr_http2_session_encrypted",
|
||||
"http2.sessionType": "scr_http2_session_type",
|
||||
"http2.sessionAlpn": "scr_http2_session_alpn",
|
||||
"http2.sessionSocket": "scr_http2_session_socket",
|
||||
"http2.streamRespond": "scr_http2_stream_respond",
|
||||
"http2.streamWrite": "scr_http2_stream_write_str",
|
||||
"http2.streamWriteBytes": "scr_http2_stream_write_bytes",
|
||||
"http2.streamEnd": "scr_http2_stream_end",
|
||||
"http2.streamEndStr": "scr_http2_stream_end_str",
|
||||
"http2.streamEndBytes": "scr_http2_stream_end_bytes",
|
||||
"http2.streamDestroy": "scr_http2_stream_destroy",
|
||||
"http2.streamSetEncoding": "scr_http2_stream_set_encoding",
|
||||
"http2.streamSetEncodingRet": "scr_http2_stream_set_encoding_ret",
|
||||
"http2.streamResume": "scr_http2_stream_resume",
|
||||
"http2.streamPause": "scr_http2_stream_pause",
|
||||
"http2.streamId": "scr_http2_stream_id",
|
||||
"http2.streamRstCode": "scr_http2_stream_rst_code",
|
||||
"http2.streamDestroyed": "scr_http2_stream_destroyed",
|
||||
"http2.streamClosed": "scr_http2_stream_closed",
|
||||
"http2.streamAborted": "scr_http2_stream_aborted",
|
||||
"http2.streamPending": "scr_http2_stream_pending",
|
||||
"http2.streamSession": "scr_http2_stream_session",
|
||||
"http2.streamUndefCall": "scr_http2_stream_undef_call",
|
||||
};
|
||||
const symbol = direct[e.fn];
|
||||
if (symbol !== undefined) return finishRuntimeCall(host, e, symbol, a);
|
||||
switch (e.fn) {
|
||||
case "http2.streamNoop": return { name: "", type: e.type };
|
||||
case "http2.createServerReq": return finishRuntimeCall(host, e, "scr_http2_create_server_req", requestHandler(host, args[0]!));
|
||||
case "http2.createSecureServerDyn":
|
||||
case "http2.createSecureServerDynCb":
|
||||
return finishRuntimeCall(host, e, "scr_http2_create_secure_server_dyn", [a[0]!, ...(args[1] === undefined ? [ptr(), ptr()] : requestHandler(host, args[1]))]);
|
||||
case "http2.createSecureServerH2":
|
||||
return finishRuntimeCall(host, e, "scr_http2_create_secure_server", [...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), a[2]!]);
|
||||
case "http2.createSecureServerH2Req":
|
||||
return finishRuntimeCall(host, e, "scr_http2_create_secure_server_req", [...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), ...requestHandler(host, args[2]!), a[3]!]);
|
||||
case "http2.createSecureServer":
|
||||
case "http2.createSecureServerReq":
|
||||
case "http2.createSecureServerSni": {
|
||||
let handler = [ptr(), ptr()], sni = [ptr(), ptr()];
|
||||
if (e.fn === "http2.createSecureServerReq") handler = requestHandler(host, args[2]!);
|
||||
if (e.fn === "http2.createSecureServerSni") {
|
||||
const cb = args[2]!;
|
||||
let type = cb.type, value = cb.name;
|
||||
if (type.kind === "union") {
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const tag = arms.findIndex((arm) => arm.kind === "func");
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
} else host.moveTemp(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined) throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))];
|
||||
}
|
||||
return finishRuntimeCall(host, e, "scr_http2_create_secure_server_allow_http1", [...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), ...handler, ...sni, a[e.fn === "http2.createSecureServer" ? 2 : 3]!]);
|
||||
}
|
||||
case "http2.connect":
|
||||
case "http2.connectCb": {
|
||||
const cb = args[3];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : callbackAdapter(host, cb, `scr_http2_connect_thunk${Math.min(callbackType(cb).params.length, 2)}`, ["ptr", "ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, "scr_http2_connect", [...a.slice(0, 2), ...rawBytes(host, args[2]!), ...handler]);
|
||||
}
|
||||
case "http2.sessionClose":
|
||||
case "http2.sessionCloseCb":
|
||||
case "http2.streamClose":
|
||||
case "http2.streamCloseCb": {
|
||||
const stream = e.fn.startsWith("http2.stream"), cbIndex = stream ? 2 : 1;
|
||||
if (args[cbIndex] !== undefined) host.moveTemp(args[cbIndex]!);
|
||||
return finishRuntimeCall(host, e, stream ? "scr_http2_stream_close" : "scr_http2_session_close", [...a.slice(0, cbIndex), a[cbIndex] ?? ptr()]);
|
||||
}
|
||||
case "http2.sessionSettings0":
|
||||
case "http2.sessionSettings":
|
||||
case "http2.sessionSettingsCb0": {
|
||||
const handler = args[2] === undefined ? [ptr(), ptr()] : callbackAdapter(host, args[2], "scr_http2_settings_thunk0", ["ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, "scr_http2_session_settings", [a[0]!, a[1] ?? ptr(), ...handler]);
|
||||
}
|
||||
}
|
||||
const cb = args[1];
|
||||
if (cb === undefined) throw new InternalCompilerError(`unhandled HTTP/2 call ${e.fn}`);
|
||||
const type = callbackType(cb), arity = type.params.length;
|
||||
let runtime: string, thunk: string | null = null, params: string[] = ["ptr", "ptr"];
|
||||
switch (e.fn) {
|
||||
case "http2.serverOnStream": runtime = "scr_http2_server_on_stream"; thunk = "scr_http2_stream_thunk"; params = ["ptr", "ptr", "ptr", "double"]; break;
|
||||
case "http2.sessionOnStream": runtime = "scr_http2_session_on_stream"; thunk = "scr_http2_stream_thunk"; params = ["ptr", "ptr", "ptr", "double"]; break;
|
||||
case "http2.serverOnSession": runtime = "scr_http2_server_on_session"; thunk = arity === 0 ? "scr_http2_session_thunk0" : "scr_http2_session_thunk"; break;
|
||||
case "http2.serverOnSessionError": runtime = "scr_http2_server_on_session_error"; thunk = `scr_http2_session_error_thunk${Math.min(arity, 2)}`; params = ["ptr", "ptr", "ptr"]; break;
|
||||
case "http2.sessionOnConnect": runtime = "scr_http2_session_on_connect"; thunk = `scr_http2_connect_thunk${Math.min(arity, 2)}`; params = ["ptr", "ptr", "ptr"]; break;
|
||||
case "http2.sessionOnGoaway": runtime = "scr_http2_session_on_goaway"; thunk = `scr_http2_goaway_thunk${Math.min(arity, 2)}`; params = ["ptr", "double", "double"]; break;
|
||||
case "http2.sessionOnError": runtime = "scr_http2_session_on_error"; thunk = arity === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; break;
|
||||
case "http2.streamOnError": runtime = "scr_http2_stream_on_error"; thunk = arity === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; break;
|
||||
case "http2.sessionOnSettings0": runtime = "scr_http2_session_on_settings"; thunk = "scr_http2_settings_thunk0"; break;
|
||||
case "http2.streamOnResponse": runtime = "scr_http2_stream_on_response"; thunk = "scr_http2_resp_thunk"; params = ["ptr", "ptr", "double", "double"]; break;
|
||||
case "http2.streamOnData": runtime = "scr_http2_stream_on_data"; thunk = arity === 0 ? "scr_net_data_thunk0" : type.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" : type.params[0]!.kind === "string" ? "scr_net_data_thunk_str" : "scr_net_data_thunk_bytes"; params = ["ptr", "ptr", "ptr"]; break;
|
||||
case "http2.sessionOnClose": runtime = "scr_http2_session_on_close"; break;
|
||||
case "http2.streamOnEnd": runtime = "scr_http2_stream_on_end"; break;
|
||||
case "http2.streamOnClose": runtime = "scr_http2_stream_on_close"; break;
|
||||
case "http2.streamOnAborted": runtime = "scr_http2_stream_on_aborted"; break;
|
||||
default: throw new InternalCompilerError(`unhandled HTTP/2 listener ${e.fn}`);
|
||||
}
|
||||
if (thunk === null) host.moveTemp(cb);
|
||||
const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params);
|
||||
return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { typeKey } from "../../ir/ir.js";
|
||||
import { mangleRecordRelease, mangleRecordStruct } from "../mangle.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
|
||||
export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)), cb = args[2]!;
|
||||
const lookup = callbackType(cb), answer = lookup.params[2];
|
||||
if (answer?.kind !== "func" || answer.params.length !== 2) throw new InternalCompilerError("lookup answer must have two parameters");
|
||||
const error = answer.params[0]!, addresses = answer.params[1]!;
|
||||
if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record") throw new InternalCompilerError("invalid lookup answer parameters");
|
||||
const nullTag = host.unionsById.get(error.unionId)?.arms.findIndex((arm) => arm.kind === "nullT") ?? -1;
|
||||
const shape = host.recordsById.get(addresses.elem.shapeId);
|
||||
const field = shape?.fields.findIndex((item) => item.name === "address") ?? -1;
|
||||
if (nullTag < 0 || field < 0) throw new InternalCompilerError("invalid lookup answer layouts");
|
||||
const key = "lookup:" + typeKey(answer);
|
||||
let symbol = host.resolveThunks.get(key);
|
||||
if (symbol === undefined) {
|
||||
symbol = `sc_lookup_answer_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
for (const declaration of [
|
||||
"declare ptr @scr_str_retain_v(ptr)", "declare void @scr_str_release(ptr)",
|
||||
"declare void @scr_union_release(ptr)", "declare void @scr_arr_release(ptr)",
|
||||
"declare double @scr_arr_len(ptr)", `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
"declare ptr @scr_arr_get_ref(ptr, double)", "declare double @scr_arr_push_ref(ptr, ptr)",
|
||||
"declare void @scr_net_lookup_answer(ptr, i1 zeroext, ptr, ptr)",
|
||||
]) host.declare(declaration);
|
||||
const struct = mangleRecordStruct(addresses.elem.shapeId), release = mangleRecordRelease(addresses.elem.shapeId);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${symbol}(ptr %self, ptr %error, ptr %addresses) ${FN_ATTRS} {`, "entry:",
|
||||
" %tagp = getelementptr inbounds %ScrUnion, ptr %error, i64 0, i32 1", " %tag = load i32, ptr %tagp",
|
||||
` %failed = icmp ne i32 %tag, ${nullTag}`, " br i1 %failed, label %failure, label %success", "failure:",
|
||||
" %errorp = getelementptr inbounds %ScrUnion, ptr %error, i64 0, i32 5", " %err = load ptr, ptr %errorp",
|
||||
` %messagep = getelementptr inbounds i8, ptr %err, i64 ${host.abiOffset(24, 12)}`,
|
||||
" %message = load ptr, ptr %messagep", " %owned.message = call ptr @scr_str_retain_v(ptr %message)", " br label %finish",
|
||||
"success:", " %length = call double @scr_arr_len(ptr %addresses)", ` %capacity = fptoui double %length to ${host.sizeType}`,
|
||||
` %ips = call ptr @scr_arr_new(i32 2, ${host.sizeType} %capacity)`, " br label %condition", "condition:",
|
||||
" %index = phi double [ 0.0, %success ], [ %next, %body ]", " %more = fcmp olt double %index, %length", " br i1 %more, label %body, label %finish", "body:",
|
||||
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
|
||||
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`, " %address = load ptr, ptr %addressp",
|
||||
" %owned.address = call ptr @scr_str_retain_v(ptr %address)", " call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
|
||||
` call void @${release}(ptr %record)`, " %next = fadd double %index, 1.0", " br label %condition", "finish:",
|
||||
" %msg = phi ptr [ %owned.message, %failure ], [ null, %condition ]", " %list = phi ptr [ null, %failure ], [ %ips, %condition ]",
|
||||
" call void @scr_net_lookup_answer(ptr %self, i1 zeroext %failed, ptr %msg, ptr %list)",
|
||||
" call void @scr_str_release(ptr %msg)", " call void @scr_arr_release(ptr %list)",
|
||||
" call void @scr_union_release(ptr %error)", " call void @scr_arr_release(ptr %addresses)", " ret void", "}", "",
|
||||
);
|
||||
}
|
||||
host.usesTimers = true;
|
||||
host.moveTemp(cb);
|
||||
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [...args.map((arg) => abiValue(host, arg)), ptr("@" + symbol)]);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { emitLookupConnect } from "./lib-lookup.js";
|
||||
import { emitConnectListener } from "./lib-connect.js";
|
||||
import { abiValue, callRuntime, callbackAdapter, callbackType, finishRuntimeCall, nullableReference, ptr } from "./lib-abi.js";
|
||||
import { emitTlsLibCall } from "./lib-tls.js";
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
@@ -8,6 +12,23 @@ import { f64Lit } from "./common.js";
|
||||
import { emitAlwaysThrowLibCall } from "./lib-shared.js";
|
||||
|
||||
export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (["https.createServer", "https.createServerDyn", "https.createServerDynCb", "https.requestAgent", "https.requestAgentCb", "https.requestFn", "https.requestFnCb"].includes(e.fn)) return emitTlsLibCall(host, e);
|
||||
if (e.fn === "net.connectLookup") return emitLookupConnect(host, e);
|
||||
if (e.fn === "http.serverOnConnect") return emitConnectListener(host, e);
|
||||
if (e.fn === "http.reqH2Stream") {
|
||||
const args = e.args.map((arg) => abiValue(host, host.emitExpr(arg)));
|
||||
const value = callRuntime(host, "scr_http_req_h2_stream", "ptr", args);
|
||||
return nullableReference(host, e.type, value, "http2Stream", "undefinedT");
|
||||
}
|
||||
if (e.fn === "http.requestConn" || e.fn === "http.requestConnCb") {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
host.usesTimers = true;
|
||||
host.moveTemp(args[0]!);
|
||||
const cb = args[6];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : callbackAdapter(host, cb,
|
||||
callbackType(cb).params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res", ["ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, "scr_http_request_conn", [...args.slice(0, 6).map((arg) => abiValue(host, arg)), ...handler]);
|
||||
}
|
||||
const B = host.B;
|
||||
if (e.fn === "net.connectOptsChk") {
|
||||
return emitAlwaysThrowLibCall(host, e, "scr_net_connect_opts_chk");
|
||||
@@ -194,7 +215,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, i1 ${args[2]!.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "net.serverOnConnection" || e.fn === "http.serverOnTimeout" || e.fn === "http.serverSetTimeoutCb" || e.fn === "http.clientOnSocket") {
|
||||
if (e.fn === "net.serverOnConnection" || e.fn === "net.serverOnSecureConnection" || e.fn === "http.serverOnTimeout" || e.fn === "http.serverSetTimeoutCb" || e.fn === "http.clientOnSocket") {
|
||||
const cbIndex = e.fn === "http.serverSetTimeoutCb" ? 2 : 1;
|
||||
const cbT = e.args[cbIndex]!.type;
|
||||
if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
@@ -206,7 +227,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
host.declare("declare void @scr_net_server_set_timeout_cb(ptr, double, ptr, ptr)");
|
||||
B.line(`call void @scr_net_server_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`);
|
||||
} else {
|
||||
const entry = e.fn === "net.serverOnConnection" ? "scr_net_server_on_connection"
|
||||
const entry = e.fn === "net.serverOnSecureConnection" ? "scr_net_server_on_secure_connection" : e.fn === "net.serverOnConnection" ? "scr_net_server_on_connection"
|
||||
: e.fn === "http.clientOnSocket" ? "scr_http_client_on_socket" : "scr_net_server_on_timeout";
|
||||
host.declare(`declare void @${entry}(ptr, ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr @${adapter}, i1 ${args[2]!.name})`);
|
||||
|
||||
@@ -564,8 +564,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
// (recv, name, cb /moves — the identity/, once, prepend): the
|
||||
// listener registers through scr_emitter_on_via with an emitted
|
||||
// fixed-arity adapter closure (what emit invokes) and the runtime's
|
||||
// matching va_list shim — the C backend's emitterInvokeThunkFor
|
||||
// split across the C/LLVM boundary. May-throw ('newListener' meta
|
||||
// matching va_list shim across the runtime ABI. May-throw ('newListener' meta
|
||||
// listeners run inside).
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: emitter.on listener not a func");
|
||||
|
||||
@@ -24,6 +24,9 @@ export function emitAlwaysThrowLibCall(
|
||||
}
|
||||
|
||||
export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
|
||||
"insp.jsonDyn": "scr_dyn_format_j",
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
"weakMap.is": "scr_weak_map_is",
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callRuntime, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)), a = args.map((arg) => abiValue(host, arg));
|
||||
host.usesTimers = true;
|
||||
switch (e.fn) {
|
||||
case "test.register":
|
||||
host.moveTemp(args[3]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_register", a);
|
||||
case "test.registerEmpty":
|
||||
return finishRuntimeCall(host, e, "scr_test_register", [...a.slice(0, 3), ptr(), ...a.slice(3)]);
|
||||
case "test.suite":
|
||||
host.moveTemp(args[3]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_suite", a);
|
||||
case "test.hook":
|
||||
host.moveTemp(args[1]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_hook", a);
|
||||
case "test.sub":
|
||||
host.moveTemp(args[4]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_sub", a);
|
||||
case "test.subEmpty": {
|
||||
const promise = callRuntime(host, "scr_test_sub", "ptr", [...a.slice(0, 4), ptr(), { type: "double", name: f64Lit(0) }, a[4]!]);
|
||||
callRuntime(host, "scr_promise_release", "void", [ptr(promise)]);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "test.ctxSkip": return finishRuntimeCall(host, e, "scr_test_ctx_skip", a);
|
||||
case "test.ctxTodo": return finishRuntimeCall(host, e, "scr_test_ctx_todo", a);
|
||||
case "test.ctxDiagnostic": return finishRuntimeCall(host, e, "scr_test_ctx_diagnostic", a);
|
||||
case "test.ctxName": return finishRuntimeCall(host, e, "scr_test_ctx_name", a);
|
||||
default: throw new InternalCompilerError(`unhandled test call ${e.fn}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callRuntime, callbackAdapter, callbackType, finishRuntimeCall, nullableReference, ptr, rawBytes, requestHandler } from "./lib-abi.js";
|
||||
import { emitAlwaysThrowLibCall } from "./lib-shared.js";
|
||||
|
||||
export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (e.fn === "tls.caCertsChk") return emitAlwaysThrowLibCall(host, e, "scr_tls_ca_certs_chk");
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
const a = args.map((arg) => abiValue(host, arg));
|
||||
host.usesTimers = true;
|
||||
switch (e.fn) {
|
||||
case "tls.createServer":
|
||||
case "tls.createServerCb":
|
||||
case "https.createServer": {
|
||||
const cb = args[2];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : e.fn === "https.createServer" ? requestHandler(host, cb)
|
||||
: callbackAdapter(host, cb, callbackType(cb).params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock", ["ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, e.fn === "https.createServer" ? "scr_https_create_server" : "scr_tls_create_server", [
|
||||
...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), ...handler,
|
||||
]);
|
||||
}
|
||||
case "tls.createServerDyn":
|
||||
case "tls.createServerDynCb":
|
||||
case "https.createServerDyn":
|
||||
case "https.createServerDynCb": {
|
||||
const cb = args[1];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : e.fn.startsWith("https.") ? requestHandler(host, cb)
|
||||
: callbackAdapter(host, cb, callbackType(cb).params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock", ["ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, e.fn.startsWith("https.") ? "scr_https_create_server_dyn" : "scr_tls_create_server_dyn", [a[0]!, ...handler]);
|
||||
}
|
||||
case "tls.createSecureContext":
|
||||
return finishRuntimeCall(host, e, "scr_tls_create_secure_context", [...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!)]);
|
||||
case "tls.createSecureContextDyn": return finishRuntimeCall(host, e, "scr_tls_create_secure_context_dyn", a);
|
||||
case "tls.pemDyn": return finishRuntimeCall(host, e, "scr_tls_pem_from_dyn", [a[0]!, rawBytes(host, args[1]!)[0]!]);
|
||||
case "tls.sockAuthorized": return finishRuntimeCall(host, e, "scr_tls_sock_authorized", a);
|
||||
case "tls.sockAuthError": {
|
||||
const result = callRuntime(host, "scr_tls_sock_auth_error", "ptr", a);
|
||||
return nullableReference(host, e.type, result, "string", "nullT");
|
||||
}
|
||||
case "tls.connect":
|
||||
case "tls.connectCb":
|
||||
if (args[3] !== undefined) host.moveTemp(args[3]);
|
||||
return finishRuntimeCall(host, e, "scr_tls_connect_dyn", [...a.slice(0, 3), a[3] ?? ptr()]);
|
||||
case "tls.sockOnSecureConnect":
|
||||
host.moveTemp(args[1]!);
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a);
|
||||
case "tls.sockOnSession": {
|
||||
const cb = args[1]!, type = callbackType(cb);
|
||||
const adapter = type.params.length === 0 ? "scr_net_data_thunk0" : type.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" : "scr_net_data_thunk_bytes";
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_on_session", [a[0]!, ...callbackAdapter(host, cb, adapter, ["ptr", "ptr", "ptr"]), a[2]!]);
|
||||
}
|
||||
case "https.requestAgent":
|
||||
case "https.requestAgentCb": {
|
||||
const cb = args[10];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : callbackAdapter(host, cb,
|
||||
callbackType(cb).params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res", ["ptr", "ptr"]);
|
||||
return finishRuntimeCall(host, e, "scr_https_request_agent", [...a.slice(0, 8), ...rawBytes(host, args[8]!), a[9]!, ...handler]);
|
||||
}
|
||||
case "https.requestFn":
|
||||
case "https.requestFnCb": {
|
||||
const cb = args[10];
|
||||
const handler = cb === undefined ? [ptr(), ptr()] : callbackAdapter(host, cb,
|
||||
callbackType(cb).params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res", ["ptr", "ptr"]);
|
||||
// Evaluate arguments once before selecting which transport consumes the callback.
|
||||
const pem = rawBytes(host, args[9]!);
|
||||
const B = host.B, tls = B.newLabel("request.tls"), plain = B.newLabel("request.plain"), done = B.newLabel("request.join");
|
||||
B.condBr(args[0]!.name, tls, plain);
|
||||
B.startBlock(tls);
|
||||
const secure = callRuntime(host, "scr_https_request", "ptr", [...a.slice(1, 9), ...pem, ...handler]);
|
||||
B.br(done);
|
||||
B.startBlock(plain);
|
||||
const insecure = callRuntime(host, "scr_http_request_ex", "ptr", [...a.slice(1, 8), ...handler, { type: "i32", name: "80" }, ptr(), ptr()]);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ptr [ ${secure}, %${tls} ], [ ${insecure}, %${plain} ]`);
|
||||
const out = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
default: throw new InternalCompilerError(`unhandled TLS runtime call ${e.fn}`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,7 @@
|
||||
/* Type-directed RC/trace/box dispatch tables of the LLVM backend, the
|
||||
* cycle-capability fixpoint, and per-record-shape emission — the .ll
|
||||
* mirror of the C emitter's types.ts + shapes.ts slice that the
|
||||
* phase-2 tier needs. Everything here follows the SAME contracts the C
|
||||
* backend compiled into the runtime: `_v` adapters where a container
|
||||
* stores RC entry points as data, per-shape retain/release/new (and
|
||||
* trace/teardown for cycle-capable shapes) with `size_t rc` at offset 0,
|
||||
* scr_obj_alloc_note/scr_obj_free_note bracketing every shape allocation
|
||||
* so the sanitized lane's RC audit stays exact.
|
||||
*
|
||||
* Anything outside the tier refuses loudly (LlvmUnsupportedError naming
|
||||
* the type kind) — the tables never guess. */
|
||||
/** Type-directed reference counting, tracing, boxes and record layouts.
|
||||
* Containers store erased adapters; shape helpers retain, release and trace
|
||||
* their fields. Every allocation has a size_t reference count at offset zero
|
||||
* and participates in the runtime allocation audit. */
|
||||
import type { IrModule, IrRecordShape, IrType } from "../../ir/ir.js";
|
||||
import { isIdentityCollectionKey, isRefCounted, mapOf, POINTER_KINDS, runtimeRcStem, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING } from "../../ir/ir.js";
|
||||
import {
|
||||
@@ -111,9 +103,7 @@ export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
}
|
||||
|
||||
/** The trace entry point for a payload/field type, or null when the type
|
||||
* cannot participate in a cycle — traceAdapterC's table over the LLVM
|
||||
* tier's kinds (promise/object rows are out of tier and unreachable:
|
||||
* their RC rows refuse first). */
|
||||
* cannot participate in a cycle. */
|
||||
export function traceAdapter(host: ShapeHost, t: IrType): string | null {
|
||||
switch (t.kind) {
|
||||
case "dyn":
|
||||
@@ -209,7 +199,7 @@ function elemKindNum(elem: IrType): number {
|
||||
}
|
||||
}
|
||||
|
||||
/** Array construction call text (arrNewC's dispatch): ref elements
|
||||
/** Array construction call text: ref elements
|
||||
* (records, unions, closures — and cycle-capable inner arrays, whose
|
||||
* SCR_ELEM_ARR spelling would hide them from the outer array's trace)
|
||||
* construct through scr_arr_new_ref with the element type's `_v` RC entry
|
||||
@@ -244,7 +234,7 @@ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): stri
|
||||
* data) for per-shape payloads and cycle-capable arrays. SCR_BOX_* tags
|
||||
* from scr_runtime.h. */
|
||||
export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
const plain: Partial<Record<IrType["kind"], number>> = { f64: 0, date: 0, bool: 1, string: 2, func: 4 };
|
||||
const plain: Partial<Record<IrType["kind"], number>> = { f64: 0, date: 0, procStream: 0, bool: 1, string: 2, func: 4 };
|
||||
const kind = plain[t.kind];
|
||||
if (kind !== undefined) {
|
||||
host.declare(`declare ptr @scr_box_new(i32)`);
|
||||
@@ -254,22 +244,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
host.declare(`declare ptr @scr_box_new(i32)`);
|
||||
return `call ptr @scr_box_new(i32 3)`; // SCR_BOX_ARR
|
||||
}
|
||||
if (
|
||||
t.kind === "record" || t.kind === "object" || t.kind === "classval" || t.kind === "union" ||
|
||||
t.kind === "array" || t.kind === "map" || t.kind === "set" || t.kind === "symbol" || t.kind === "bigint" || t.kind === "regex" ||
|
||||
t.kind === "promise" || t.kind === "bytes" || t.kind === "url" || t.kind === "searchParams" ||
|
||||
t.kind === "stats" || t.kind === "fileHandle" || t.kind === "spawnRes" || t.kind === "child" || t.kind === "childStream" || t.kind === "childWriter" ||
|
||||
t.kind === "generator" ||
|
||||
t.kind === "netServer" || t.kind === "netSocket" || t.kind === "dgramSocket" ||
|
||||
t.kind === "httpReq" || t.kind === "httpRes" || t.kind === "httpClientReq" ||
|
||||
t.kind === "secureCtx" || t.kind === "cryptoHash" || t.kind === "cryptoHmac" || t.kind === "testCtx" ||
|
||||
// Island handles: the box carries scr_jsval_retain_v/release_v and
|
||||
// no trace — the same stance as jsval array elements.
|
||||
t.kind === "jsval" ||
|
||||
// Checked values trace native objects, arrays and captured closures.
|
||||
t.kind === "dyn" ||
|
||||
t.kind === "fsWatcher"
|
||||
) {
|
||||
if (isRefCounted(t)) {
|
||||
const v = vAdapters(host, t);
|
||||
host.declare(`declare ptr @scr_box_new_obj(ptr, ptr, ptr)`);
|
||||
return `call ptr @scr_box_new_obj(ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, t)})`;
|
||||
@@ -279,7 +254,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
|
||||
/** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */
|
||||
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
return t.kind === "f64" || t.kind === "date" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/* ── record shapes ────────────────────────────────────────────────────── */
|
||||
@@ -287,7 +262,7 @@ export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
/** A record field's in-struct LLVM type. bool fields store as i8 (the C
|
||||
* _Bool layout); loads/stores convert at the access site. */
|
||||
export function llFieldType(t: IrType): "double" | "i8" | "ptr" {
|
||||
if (POINTER_KINDS.has(t.kind) && t.kind !== "http2Session" && t.kind !== "http2Stream") return "ptr";
|
||||
if (POINTER_KINDS.has(t.kind)) return "ptr";
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
@@ -405,7 +380,7 @@ export function releaseBody(
|
||||
|
||||
/** Per-record-shape LLVM emission: the named struct types (returned as
|
||||
* `typeDefs`) and the new/retain/release (+trace/gcFree for cycle-capable
|
||||
* shapes) function definitions (`defs`). Layout mirrors the C emitter:
|
||||
* shapes) function definitions (`defs`). Layout follows the runtime ABI:
|
||||
* `{ i64 rc, fields..., [ptr overflow] }`; the retain/release signatures
|
||||
* are already `_v`-shaped, so the same symbols serve as container RC
|
||||
* entry points. */
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
/* The LLVM backend's refusal: an IR construct outside the current tier.
|
||||
* `kind` is a stable machine-readable tag ("stmt:switch", "expr:closure",
|
||||
* "type:union", ...) — the differential harness histograms the next
|
||||
* phase's queue from it. Never catch-and-continue: any refusal aborts the
|
||||
* whole module. compile()'s DEFAULT lane catches exactly this error and
|
||||
* re-emits through the C backend (the transparent fallback); an explicit
|
||||
* backend "llvm" surfaces it as diagnostic SC3001 instead — the
|
||||
* fail-loudly pin. Its own module so the emitter and the shape/RC tables
|
||||
* can both throw it without an import cycle. */
|
||||
/** A valid IR construct missing LLVM lowering. Compilation stops with SC3001. */
|
||||
import type { SrcLoc } from "../../ir/ir.js";
|
||||
|
||||
export class LlvmUnsupportedError extends Error {
|
||||
@@ -15,8 +7,7 @@ export class LlvmUnsupportedError extends Error {
|
||||
readonly loc?: SrcLoc,
|
||||
) {
|
||||
super(
|
||||
`the LLVM backend does not support this construct yet (${kind}); ` +
|
||||
`build with the debugging C backend (--backend c, or the native default's transparent fallback)`,
|
||||
`the LLVM backend does not support this construct yet (${kind})`,
|
||||
);
|
||||
this.name = "LlvmUnsupportedError";
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function mangleFnClosure(fnName: string): string {
|
||||
return `sc_fc_${sanitize(fnName)}`;
|
||||
}
|
||||
|
||||
/** Per-class emitted C struct and RC helpers. */
|
||||
/** Per-class emitted LLVM struct and RC helpers. */
|
||||
export function mangleClassStruct(className: string): string {
|
||||
return `sc_o_${sanitize(className)}`;
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function mangleClassGcFree(className: string): string {
|
||||
return `sc_gcfree_${sanitize(className)}`;
|
||||
}
|
||||
|
||||
/** Per-record-shape emitted C struct and RC helpers. Shape ids are
|
||||
/** Per-record-shape emitted LLVM struct and RC helpers. Shape ids are
|
||||
* compiler-generated (`r0`, `r1`, ...), but the prefixes are still disjoint
|
||||
* from every class prefix above (sc_rs_X vs sc_o_Y, sc_rnew_X vs
|
||||
* sc_new_Y, ...) so no user class name can collide with a record symbol. */
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, VOID, funcOf, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrStmt } from "../../ir/ir.js";
|
||||
import { F64, VOID, funcOf, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrStmt } from "../ir/ir.js";
|
||||
import { computeMayThrow } from "./may-throw.js";
|
||||
|
||||
const loc = { file: "tdz.ts", start: 0, end: 1 };
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
/* Cheap whole-module may-throw analysis (see computeMayThrow). Pure function
|
||||
* of the IR module; the emitter consults the result to place unwind checks. */
|
||||
import type { IrExpr, IrStmt, IrModule } from "../../ir/ir.js";
|
||||
import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS, MAY_THROW_LIB_FNS } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { hasRetainedFfiCallback } from "../ffi-callbacks.js";
|
||||
import type { IrExpr, IrStmt, IrModule } from "../ir/ir.js";
|
||||
import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS, MAY_THROW_LIB_FNS } from "../ir/ir.js";
|
||||
import { everyStmtList } from "../ir/traverse.js";
|
||||
import { hasRetainedFfiCallback } from "./ffi-callbacks.js";
|
||||
|
||||
/** Cheap may-throw analysis (cost discipline: functions that transitively
|
||||
* CANNOT throw pay for no pending-exception checks). A function may throw
|
||||
@@ -77,7 +77,7 @@ function request(root: string, packageJson: string, output = join(root, "program
|
||||
};
|
||||
}
|
||||
|
||||
test("resolves the target's matching platform helper rather than a fixed macOS package", async () => {
|
||||
test("resolves the build host's platform helper", async () => {
|
||||
const pkg = await fakePackage();
|
||||
const output = join(pkg.root, "linux.o");
|
||||
await writeFile(pkg.packageJson, JSON.stringify({ name: LINUX_X64_GNU_TARGET.helperPackage }));
|
||||
@@ -88,7 +88,7 @@ test("resolves the target's matching platform helper rather than a fixed macOS p
|
||||
.replaceAll(`"supported_targets":["${MACOS_ARM64_TARGET.llvmTriple}"]`, `"supported_targets":["${LINUX_X64_GNU_TARGET.llvmTriple}"]`));
|
||||
await emitNativeArtifact({
|
||||
...request(pkg.root, pkg.packageJson, output, LINUX_X64_GNU_TARGET),
|
||||
helperHost: { platform: "linux", arch: "x64" },
|
||||
helperHost: { platform: "linux", arch: "x64", linuxLibc: "gnu" },
|
||||
});
|
||||
expect(await readFile(output, "utf8")).toContain("define i32 @answer");
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface NativeCodegenOptions {
|
||||
sanitize?: boolean;
|
||||
target?: NativeTargetSpec;
|
||||
/** Test seam for package selection on a simulated host. */
|
||||
helperHost?: { platform: NodeJS.Platform; arch: string };
|
||||
helperHost?: { platform: NodeJS.Platform; arch: string; linuxLibc?: "gnu" | "musl" };
|
||||
/** Test seam: still resolves a package path, never searches PATH. */
|
||||
resolvePackageJson?: (specifier: string) => string;
|
||||
/** Internal/test override; omitted production calls use the shared cache. */
|
||||
@@ -122,12 +122,13 @@ export function validateNativeCodegenVersion(
|
||||
async function resolveHelper(
|
||||
target: NativeTargetSpec,
|
||||
resolver?: (specifier: string) => string,
|
||||
host?: { platform: NodeJS.Platform; arch: string },
|
||||
host?: { platform: NodeJS.Platform; arch: string; linuxLibc?: "gnu" | "musl" },
|
||||
): Promise<ResolvedHelper> {
|
||||
const helper = nativeHelperForTarget(
|
||||
target,
|
||||
host?.platform,
|
||||
host?.arch,
|
||||
host?.linuxLibc,
|
||||
);
|
||||
if (helper === null) {
|
||||
throw new NativeCodegenError(
|
||||
@@ -191,7 +192,7 @@ async function resolveHelper(
|
||||
// host-native ISA plus WebAssembly). Cache only after the target backend
|
||||
// has also been checked; otherwise a prior X86 lookup could accidentally
|
||||
// bless a later WASI request against an older X86-only package.
|
||||
const cacheKey = JSON.stringify({ dependencies, targetBackend: target.llvmBackend });
|
||||
const cacheKey = JSON.stringify({ dependencies, targetTriple: target.llvmTriple });
|
||||
const load = async (): Promise<ResolvedHelper> => {
|
||||
let stdout: string;
|
||||
let binary: Buffer;
|
||||
|
||||
@@ -1,182 +1,55 @@
|
||||
import { mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { loadFfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { createNativeLinkInfo, type NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { MACOS_ARM64_TARGET, WASM32_WASI_TARGET } from "./targets.js";
|
||||
import { loadRuntimePack } from "./runtime-pack.js";
|
||||
|
||||
const BASE: NativeLinkFeatures = {
|
||||
dynamic: false,
|
||||
regex: false,
|
||||
copying: false,
|
||||
textDecoderLegacy: false,
|
||||
fileHandle: false,
|
||||
fetch: false,
|
||||
netIsland: false,
|
||||
zlib: false,
|
||||
assert: false,
|
||||
inspect: false,
|
||||
dynInvoke: false,
|
||||
dc: false,
|
||||
dynAsync: false,
|
||||
events: false,
|
||||
emitter: false,
|
||||
symbol: false,
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
};
|
||||
vi.mock("./runtime-pack.js", () => ({
|
||||
loadRuntimePack: vi.fn(async ({ target, optimization }) => ({
|
||||
root: "/installed/runtime", manifest: { package: target.runtimePackPackage, version: "1.2.3" },
|
||||
flavor: optimization, runtimeObjects: ["/installed/runtime/base.o"], archives: ["/installed/runtime/vendor.a"],
|
||||
selectedRuntimeArtifacts: [{ path: "base.o", sha256: "1".repeat(64), size: 42 }],
|
||||
selectedArchiveArtifacts: [{ path: "vendor.a", sha256: "2".repeat(64), size: 84 }],
|
||||
systemLibraries: [...target.runtimeSystemLibraries],
|
||||
})),
|
||||
}));
|
||||
const features = {} as NativeLinkFeatures;
|
||||
|
||||
describe("native link info recipes", () => {
|
||||
test("FFI frameworks remain Darwin-only and callback adapters are not external symbols", async () => {
|
||||
const ffi = { ffiFormat: 7 as const, libraries: [], systemLibraries: [], frameworks: ["Foundation"], functions: [
|
||||
{ name: "native", symbol: "native", params: [], returns: "void" as const },
|
||||
{ name: "release", symbol: "release", params: [], returns: "void" as const, callbackOperation: "release" as const },
|
||||
] };
|
||||
const info = await createNativeLinkInfo({ programObject: "/out/app.o", target: MACOS_ARM64_TARGET, features: BASE, ffi });
|
||||
describe("native link info", () => {
|
||||
test("reports verified precompiled inputs with their integrity metadata and link order", async () => {
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o", target: MACOS_ARM64_TARGET, features,
|
||||
ffi: { ffiFormat: 7, libraries: ["/ffi/native.a"], systemLibraries: ["sqlite3"], frameworks: ["Foundation"], functions: [
|
||||
{ name: "native", symbol: "native", params: [], returns: "void" },
|
||||
{ name: "release", symbol: "release", params: [], returns: "void", callbackOperation: "release" },
|
||||
] },
|
||||
});
|
||||
expect(info.runtime_pack).toEqual({
|
||||
kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", version: "1.2.3", root: "/installed/runtime",
|
||||
path_base: "runtime_pack.root", flavor: "release",
|
||||
objects: [{ path: "base.o", sha256: "1".repeat(64), size: 42 }],
|
||||
archives: [{ path: "vendor.a", sha256: "2".repeat(64), size: 84 }],
|
||||
});
|
||||
expect(info.ffi.symbols).toEqual(["native"]);
|
||||
expect(info.link.frameworks).toEqual(["Foundation"]);
|
||||
await expect(createNativeLinkInfo({ programObject: "/out/app.o", target: WASM32_WASI_TARGET, features: BASE, ffi })).rejects.toThrow("Darwin");
|
||||
});
|
||||
test("FileHandle-only links include checked promise adapters without an engine", async () => {
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o", target: MACOS_ARM64_TARGET,
|
||||
features: { ...BASE, fileHandle: true }, ffi: null,
|
||||
});
|
||||
const sources = info.runtime_pack.source_sets.find((set) => set.name === "runtime")!.sources;
|
||||
expect(sources).toContain("src/scr_file_handle.c");
|
||||
expect(sources).toContain("src/scr_async_dyn.c");
|
||||
expect(sources).not.toContain("src/scr_island.c");
|
||||
});
|
||||
|
||||
test("feature source sets reproduce runtime and vendor gates", async () => {
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: MACOS_ARM64_TARGET,
|
||||
features: {
|
||||
...BASE,
|
||||
dynamic: true,
|
||||
regex: true,
|
||||
assert: true,
|
||||
inspect: true,
|
||||
bigint: true,
|
||||
zlib: true,
|
||||
tls: true,
|
||||
},
|
||||
ffi: null,
|
||||
});
|
||||
const runtime = info.runtime_pack.source_sets.find((set) => set.name === "runtime")!;
|
||||
expect(runtime.sources).toEqual(expect.arrayContaining([
|
||||
"src/scr_regex.c",
|
||||
"src/scr_assert.c",
|
||||
"src/scr_inspect.c",
|
||||
"src/scr_bigint.c",
|
||||
"src/scr_bigint_assert.c",
|
||||
"src/scr_zlib.c",
|
||||
"src/scr_zlib_island.c",
|
||||
"src/scr_tls.c",
|
||||
"src/scr_tls_ca.c",
|
||||
"src/scr_island.c",
|
||||
"src/scr_web.c",
|
||||
"src/scr_inspect_island.c",
|
||||
]));
|
||||
expect(info.runtime_pack.source_sets.map((set) => set.name)).toEqual([
|
||||
"runtime", "quickjs", "mbedtls",
|
||||
]);
|
||||
expect(info.runtime_pack.source_sets.find((set) => set.name === "mbedtls")!.sources.length)
|
||||
.toBeGreaterThan(100);
|
||||
expect(info.link.system_libraries).toEqual(["System", "z", "m"]);
|
||||
});
|
||||
|
||||
test("dev object recipes keep runtime optimization in lockstep", async () => {
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: MACOS_ARM64_TARGET,
|
||||
features: BASE,
|
||||
ffi: null,
|
||||
optimization: "dev",
|
||||
});
|
||||
expect(info.runtime_pack.source_sets[0]?.c_flags).toContain("-O0");
|
||||
expect(info.runtime_pack.source_sets[0]?.c_flags).not.toContain("-O2");
|
||||
// External objects are compiled separately and the driver recipe is an
|
||||
// executable link. The two halves must not leak into each other's recipe.
|
||||
expect(info.runtime_pack.source_sets[0]?.c_flags).not.toContain("-Wl,-dead_strip");
|
||||
expect(info.link.driver_flags).toContain("-Wl,-dead_strip");
|
||||
});
|
||||
|
||||
test("static external-object links dead-strip just like dynamic links", async () => {
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: MACOS_ARM64_TARGET,
|
||||
features: BASE,
|
||||
ffi: null,
|
||||
});
|
||||
expect(info.link.driver_flags).toContain("-Wl,-dead_strip");
|
||||
});
|
||||
|
||||
test("WASI external link recipes strip DWARF only in release mode", async () => {
|
||||
const release = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: WASM32_WASI_TARGET,
|
||||
features: BASE,
|
||||
ffi: null,
|
||||
optimization: "release",
|
||||
});
|
||||
const dev = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: WASM32_WASI_TARGET,
|
||||
features: BASE,
|
||||
ffi: null,
|
||||
optimization: "dev",
|
||||
});
|
||||
expect(release.link.driver_flags).toContain("-Wl,--strip-debug");
|
||||
expect(dev.link.driver_flags).not.toContain("-Wl,--strip-debug");
|
||||
});
|
||||
|
||||
test("FFI symbols and resolved inputs remain ordered before the runtime", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-native-link-info-unit-"));
|
||||
const library = join(dir, "native.o");
|
||||
const manifest = join(dir, "ffi.json");
|
||||
await writeFile(library, "fixture");
|
||||
await writeFile(manifest, JSON.stringify({
|
||||
ffi_format: 1,
|
||||
functions: [{
|
||||
name: "nativeScale",
|
||||
symbol: "native_scale",
|
||||
params: ["f64"],
|
||||
returns: "f64",
|
||||
}],
|
||||
libraries: ["./native.o"],
|
||||
system_libraries: ["sqlite3"],
|
||||
}));
|
||||
const loaded = loadFfiProfile(manifest);
|
||||
if (!loaded.ok) throw new Error(loaded.diagnostics[0]?.message);
|
||||
const info = await createNativeLinkInfo({
|
||||
programObject: "/out/app.o",
|
||||
target: MACOS_ARM64_TARGET,
|
||||
features: BASE,
|
||||
ffi: loaded.profile,
|
||||
});
|
||||
expect(info.ffi).toEqual({
|
||||
format: 1,
|
||||
symbols: ["native_scale"],
|
||||
libraries: [library],
|
||||
});
|
||||
expect(info.link.input_order.slice(0, 3)).toEqual([
|
||||
"/out/app.o", library, "runtime/*.o",
|
||||
]);
|
||||
expect(info.link.input_order).toEqual(["/out/app.o", "/ffi/native.a", "/installed/runtime/base.o", "/installed/runtime/vendor.a"]);
|
||||
expect(info.link.system_libraries).toEqual(["sqlite3", "System"]);
|
||||
expect(info.link.frameworks).toEqual(["Foundation"]);
|
||||
expect(info.link.driver_flags).toContain("-Wl,-dead_strip");
|
||||
expect(loadRuntimePack).toHaveBeenLastCalledWith({ target: MACOS_ARM64_TARGET, features, optimization: "release" });
|
||||
});
|
||||
|
||||
test("WASI release and dev select their runtime flavor and debug-link policy", async () => {
|
||||
for (const optimization of ["release", "dev"] as const) {
|
||||
const info = await createNativeLinkInfo({ programObject: "/out/app.o", target: WASM32_WASI_TARGET, features, ffi: null, optimization });
|
||||
expect(info.runtime_pack.flavor).toBe(optimization);
|
||||
expect(info.link.driver_flags.includes("-Wl,--strip-debug")).toBe(optimization === "release");
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects non-Darwin frameworks before selecting a pack", async () => {
|
||||
vi.mocked(loadRuntimePack).mockClear();
|
||||
await expect(createNativeLinkInfo({ programObject: "/out/app.o", target: WASM32_WASI_TARGET, features,
|
||||
ffi: { ffiFormat: 7, libraries: [], systemLibraries: [], frameworks: ["Foundation"], functions: [] },
|
||||
})).rejects.toThrow("Darwin");
|
||||
expect(loadRuntimePack).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import {
|
||||
EXECUTABLE_RUNTIME_SOURCES,
|
||||
executableSectionEliminationFlags,
|
||||
runtimeSrcDir,
|
||||
} from "./native-toolchain.js";
|
||||
import {
|
||||
EXTERNAL_OBJECT_ABI_STABILITY,
|
||||
RUNTIME_ABI_MARKER,
|
||||
RUNTIME_ABI_VERSION,
|
||||
} from "./runtime-abi.js";
|
||||
import {
|
||||
executableOptimizationLinkerArgs,
|
||||
type NativeTargetSpec,
|
||||
} from "./targets.js";
|
||||
import { LRE_SOURCES, QJS_ENGINE_SOURCES } from "./vendor-archives.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;
|
||||
@@ -51,16 +39,6 @@ export interface NativeLinkFeatures {
|
||||
tlsCa: boolean;
|
||||
}
|
||||
|
||||
export interface NativeSourceSet {
|
||||
name: "runtime" | "libregexp" | "quickjs" | "mbedtls";
|
||||
output: "objects" | "archive";
|
||||
suggested_output: string;
|
||||
sources: string[];
|
||||
include_directories: string[];
|
||||
defines: string[];
|
||||
c_flags: string[];
|
||||
}
|
||||
|
||||
export interface NativeLinkInfo {
|
||||
schema: "scriptc.native-link-info.v1";
|
||||
format: 1;
|
||||
@@ -87,12 +65,14 @@ export interface NativeLinkInfo {
|
||||
marker: typeof RUNTIME_ABI_MARKER;
|
||||
};
|
||||
runtime_pack: {
|
||||
kind: "source";
|
||||
package: "@scriptc/runtime";
|
||||
kind: "precompiled";
|
||||
package: string;
|
||||
version: string;
|
||||
root: string;
|
||||
path_base: "runtime_pack.root";
|
||||
source_sets: NativeSourceSet[];
|
||||
flavor: "release" | "dev";
|
||||
objects: RuntimePackArtifact[];
|
||||
archives: RuntimePackArtifact[];
|
||||
};
|
||||
ffi: {
|
||||
format: number | null;
|
||||
@@ -107,183 +87,6 @@ export interface NativeLinkInfo {
|
||||
};
|
||||
}
|
||||
|
||||
function unique<T>(values: readonly T[]): T[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function packagePath(...parts: string[]): string {
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
function runtimeSourceRecipe(
|
||||
features: NativeLinkFeatures,
|
||||
target: NativeTargetSpec,
|
||||
env: NodeJS.ProcessEnv,
|
||||
optimization: "release" | "dev",
|
||||
): { sets: NativeSourceSet[]; systemLibraries: string[]; driverFlags: string[] } {
|
||||
const dynamic = features.dynamic;
|
||||
const executableSectionFlags = executableSectionEliminationFlags(target.platform);
|
||||
const curlFetch = dynamic && features.fetch && env["SCRIPTC_FETCH_CURL"] === "1";
|
||||
const nativeFetch = features.fetch && !curlFetch;
|
||||
const netIsland = dynamic && (features.netIsland || nativeFetch);
|
||||
const net = features.net || nativeFetch || netIsland;
|
||||
const http = features.http || nativeFetch || netIsland;
|
||||
const tls = features.tls || nativeFetch || netIsland;
|
||||
const tlsCa = features.tlsCa || tls;
|
||||
const wasiUnavailableSources = new Set([
|
||||
"scr_child.c", "scr_dyn_handle.c", "scr_loop_kqueue.c", "scr_loop_epoll.c",
|
||||
"scr_loop_wsapoll.c", "scr_net.c", "scr_http.c", "scr_http2.c", "scr_dgram.c",
|
||||
"scr_watch.c", "scr_tls_ca.c", "scr_tls.c", "scr_fetch.c", "scr_net_island.c",
|
||||
]);
|
||||
const runtimeSources = [
|
||||
...EXECUTABLE_RUNTIME_SOURCES.filter((source) =>
|
||||
target.platform !== "wasi" || !wasiUnavailableSources.has(source)
|
||||
),
|
||||
...(target.platform === "win32" ? ["scr_win.c"] : []),
|
||||
...(target.name === "linux-x64-musl" || target.name === "linux-arm64-musl"
|
||||
? ["scr_musl.c"]
|
||||
: []),
|
||||
...(features.copying ? ["scr_copying.c"] : []),
|
||||
...(features.fileHandle ? ["scr_file_handle.c"] : []),
|
||||
...(features.regex ? ["scr_regex.c"] : []),
|
||||
...(features.assert || features.regex || features.symbol ? ["scr_assert.c"] : []),
|
||||
...(features.inspect ? ["scr_inspect.c"] : []),
|
||||
...(features.dynInvoke || nativeFetch ? ["scr_dyn_invoke.c"] : []),
|
||||
...(features.dc ? ["scr_dc.c"] : []),
|
||||
...(features.dynAsync || features.dynInvoke || features.dc || features.fileHandle || nativeFetch
|
||||
? ["scr_async_dyn.c"]
|
||||
: []),
|
||||
...(features.zlib ? ["scr_zlib.c"] : []),
|
||||
...(features.zlib && dynamic ? ["scr_zlib_island.c"] : []),
|
||||
...(features.events ? ["scr_events.c", "scr_readline.c"] : []),
|
||||
...(features.emitter ? ["scr_events_emitter.c"] : []),
|
||||
...(features.emitter || net ? ["scr_dyn_handle.c"] : []),
|
||||
...(features.symbol ? ["scr_symbol.c"] : []),
|
||||
...(features.assert && features.bigint ? ["scr_bigint_assert.c"] : []),
|
||||
...(features.searchParams ? ["scr_url_params.c"] : []),
|
||||
...(features.qs ? ["scr_qs.c"] : []),
|
||||
...(features.parseArgs ? ["scr_util.c"] : []),
|
||||
...(features.stream ? ["scr_stream.c"] : []),
|
||||
...(net || features.dgram
|
||||
? ["scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c"]
|
||||
: []),
|
||||
...(net ? ["scr_net.c"] : []),
|
||||
...(http ? ["scr_http.c"] : []),
|
||||
...(features.http2 ? ["scr_http2.c"] : []),
|
||||
...(features.dgram ? ["scr_dgram.c"] : []),
|
||||
...(features.watch ? ["scr_watch.c"] : []),
|
||||
...(features.foreignFfi ? ["scr_ffi_queue.c"] : []),
|
||||
...(features.nodeTest ? ["scr_test.c"] : []),
|
||||
...(tlsCa ? ["scr_tls_ca.c"] : []),
|
||||
...(tls ? ["scr_tls.c"] : []),
|
||||
...(nativeFetch ? ["scr_fetch.c"] : []),
|
||||
...(dynamic
|
||||
? [
|
||||
"scr_island.c",
|
||||
"scr_web.c",
|
||||
...(features.inspect ? ["scr_inspect_island.c"] : []),
|
||||
...(netIsland ? ["scr_net_island.c"] : []),
|
||||
...(curlFetch ? ["scr_fetch_curl.c"] : []),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const filteredSources = target.platform === "wasi"
|
||||
? runtimeSources.filter((source) => !wasiUnavailableSources.has(source))
|
||||
: runtimeSources;
|
||||
const sets: NativeSourceSet[] = [{
|
||||
name: "runtime",
|
||||
output: "objects",
|
||||
suggested_output: "runtime/*.o",
|
||||
sources: unique(filteredSources).map((source) => packagePath("src", source)),
|
||||
include_directories: unique([
|
||||
"src",
|
||||
...(features.regex || dynamic ? ["vendor/quickjs-ng"] : []),
|
||||
...(tls ? ["vendor/mbedtls/include"] : []),
|
||||
...(curlFetch ? ["vendor/curl/include"] : []),
|
||||
]),
|
||||
defines: unique([
|
||||
...target.runtimeCompileDefines,
|
||||
...(features.textDecoderLegacy ? ["SCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(dynamic ? ["SCR_DYNAMIC"] : []),
|
||||
]),
|
||||
c_flags: [
|
||||
"-std=c11", "-target", target.llvmTriple,
|
||||
...(target.platform === "win32" || target.platform === "wasi" ? [] : ["-pthread"]),
|
||||
optimization === "dev" ? "-O0" : "-O2",
|
||||
...executableSectionFlags.compile,
|
||||
"-fno-math-errno", "-fno-strict-aliasing", "-Wno-deprecated-declarations",
|
||||
],
|
||||
}];
|
||||
if (features.regex && !dynamic) {
|
||||
sets.push({
|
||||
name: "libregexp",
|
||||
output: "archive",
|
||||
suggested_output: "libscriptc-regexp.a",
|
||||
sources: LRE_SOURCES.map((source) => packagePath("vendor", "quickjs-ng", source)),
|
||||
include_directories: ["vendor/quickjs-ng"],
|
||||
defines: [],
|
||||
c_flags: [
|
||||
"-std=c11", "-target", target.llvmTriple,
|
||||
...target.runtimeCompileDefines.map((define) => `-D${define}`),
|
||||
"-Os",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (dynamic) {
|
||||
sets.push({
|
||||
name: "quickjs",
|
||||
output: "archive",
|
||||
suggested_output: "libscriptc-quickjs.a",
|
||||
sources: QJS_ENGINE_SOURCES.map((source) => packagePath("vendor", "quickjs-ng", source)),
|
||||
include_directories: ["vendor/quickjs-ng"],
|
||||
defines: ["QUICKJS_NG_BUILD", "_GNU_SOURCE", "NDEBUG"],
|
||||
c_flags: [
|
||||
"-std=gnu11", "-target", target.llvmTriple,
|
||||
...target.runtimeCompileDefines.map((define) => `-D${define}`),
|
||||
"-fvisibility=hidden",
|
||||
"-funsigned-char", "-Os",
|
||||
],
|
||||
});
|
||||
}
|
||||
// mbedTLS is the one source set whose membership is intentionally the
|
||||
// package's complete library/*.c set. The package version and ABI marker
|
||||
// pin those bytes as one exact runtime pack.
|
||||
if (tls) {
|
||||
sets.push({
|
||||
name: "mbedtls",
|
||||
output: "archive",
|
||||
suggested_output: "libscriptc-mbedtls.a",
|
||||
sources: [],
|
||||
include_directories: ["vendor/mbedtls/include", "vendor/mbedtls/library"],
|
||||
defines: [],
|
||||
c_flags: [
|
||||
"-std=c11", "-target", target.llvmTriple,
|
||||
...target.runtimeCompileDefines.map((define) => `-D${define}`),
|
||||
"-Os",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (target.platform === "wasi") {
|
||||
const blocked = features.net || features.http || features.http2 || features.dgram ||
|
||||
features.watch || features.tls || features.tlsCa || features.fetch || features.netIsland;
|
||||
if (blocked) throw new Error("WASI native link info requested an unavailable network/TLS runtime surface");
|
||||
}
|
||||
const systemLibraries = unique([
|
||||
...target.runtimeSystemLibraries,
|
||||
...((features.zlib || nativeFetch) && target.platform !== "win32" ? ["z"] : []),
|
||||
...(dynamic && target.platform !== "win32" ? ["m"] : []),
|
||||
...(curlFetch ? ["curl"] : []),
|
||||
]);
|
||||
return {
|
||||
sets,
|
||||
systemLibraries,
|
||||
driverFlags: [
|
||||
...target.executableLinkerArgs,
|
||||
...executableOptimizationLinkerArgs(target.platform, optimization),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function createNativeLinkInfo(options: {
|
||||
programObject: string;
|
||||
target: NativeTargetSpec;
|
||||
@@ -293,27 +96,18 @@ export async function createNativeLinkInfo(options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<NativeLinkInfo> {
|
||||
if (options.ffi?.frameworks?.length && options.target.platform !== "darwin") throw new Error("FFI frameworks require a Darwin target");
|
||||
const root = dirname(runtimeSrcDir());
|
||||
const runtimeVersion = (JSON.parse(
|
||||
await readFile(join(root, "package.json"), "utf8"),
|
||||
) as { version: string }).version;
|
||||
const recipe = runtimeSourceRecipe(
|
||||
options.features,
|
||||
options.target,
|
||||
options.env ?? process.env,
|
||||
options.optimization ?? "release",
|
||||
);
|
||||
const mbedtls = recipe.sets.find((set) => set.name === "mbedtls");
|
||||
if (mbedtls !== undefined) {
|
||||
mbedtls.sources = (await readdir(join(root, "vendor", "mbedtls", "library")))
|
||||
.filter((name) => !name.startsWith(".") && name.endsWith(".c"))
|
||||
.sort()
|
||||
.map((name) => packagePath("vendor", "mbedtls", "library", name));
|
||||
}
|
||||
const pack = await loadRuntimePack({
|
||||
target: options.target, features: options.features,
|
||||
optimization: options.optimization ?? "release", ...(options.env === undefined ? {} : { env: options.env }),
|
||||
});
|
||||
const ffiLibraries = options.ffi?.libraries ?? [];
|
||||
const vendorInputs = recipe.sets
|
||||
.filter((set) => set.output === "archive")
|
||||
.map((set) => set.suggested_output);
|
||||
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 {
|
||||
schema: "scriptc.native-link-info.v1",
|
||||
format: 1,
|
||||
@@ -340,12 +134,14 @@ export async function createNativeLinkInfo(options: {
|
||||
marker: RUNTIME_ABI_MARKER,
|
||||
},
|
||||
runtime_pack: {
|
||||
kind: "source",
|
||||
package: "@scriptc/runtime",
|
||||
version: runtimeVersion,
|
||||
root,
|
||||
kind: "precompiled",
|
||||
package: pack.manifest.package,
|
||||
version: pack.manifest.version,
|
||||
root: pack.root,
|
||||
path_base: "runtime_pack.root",
|
||||
source_sets: recipe.sets,
|
||||
flavor: pack.flavor,
|
||||
objects: pack.selectedRuntimeArtifacts,
|
||||
archives: pack.selectedArchiveArtifacts,
|
||||
},
|
||||
ffi: {
|
||||
format: options.ffi?.ffiFormat ?? null,
|
||||
@@ -353,18 +149,9 @@ export async function createNativeLinkInfo(options: {
|
||||
libraries: [...ffiLibraries],
|
||||
},
|
||||
link: {
|
||||
input_order: [
|
||||
options.programObject,
|
||||
...ffiLibraries,
|
||||
"runtime/*.o",
|
||||
...vendorInputs,
|
||||
"system_libraries",
|
||||
],
|
||||
driver_flags: recipe.driverFlags,
|
||||
system_libraries: unique([
|
||||
...(options.ffi?.systemLibraries ?? []),
|
||||
...recipe.systemLibraries,
|
||||
]),
|
||||
input_order: plan.inputs,
|
||||
driver_flags: plan.driverFlags,
|
||||
system_libraries: plan.systemLibraries,
|
||||
frameworks: [...(options.ffi?.frameworks ?? [])],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3649,7 +3649,7 @@ test("library identity edits reuse the cached large program object", async () =>
|
||||
await compileLibArchive({
|
||||
cPath,
|
||||
programSource,
|
||||
identityCSource: "unsigned long long scriptc_build_id(void) { return 1; }\n",
|
||||
identityLlvmSource: "define i64 @scriptc_build_id() { ret i64 1 }\n",
|
||||
outPath,
|
||||
cacheIdentity: TEST_CACHE_IDENTITY,
|
||||
});
|
||||
@@ -3668,7 +3668,7 @@ test("library identity edits reuse the cached large program object", async () =>
|
||||
await compileLibArchive({
|
||||
cPath,
|
||||
programSource,
|
||||
identityCSource: "unsigned long long scriptc_build_id(void) { return 2; }\n",
|
||||
identityLlvmSource: "define i64 @scriptc_build_id() { ret i64 2 }\n",
|
||||
outPath,
|
||||
cacheIdentity: TEST_CACHE_IDENTITY,
|
||||
});
|
||||
@@ -4278,9 +4278,9 @@ test.skipIf(process.platform === "win32" || zigExecutable === undefined)(
|
||||
await writeFile(cPath, "int scriptc_large_program_value(void) { return 7; }\n");
|
||||
await compileLibArchive({
|
||||
cPath,
|
||||
identityCSource: [
|
||||
"unsigned long long scriptc_build_id(void) { return 2; }",
|
||||
"unsigned scriptc_abi_version(void) { return 1; }",
|
||||
identityLlvmSource: [
|
||||
"define i64 @scriptc_build_id() { ret i64 2 }",
|
||||
"define i32 @scriptc_abi_version() { ret i32 1 }",
|
||||
"",
|
||||
].join("\n"),
|
||||
outPath: archivePath,
|
||||
|
||||
@@ -280,7 +280,7 @@ export interface CcOptions {
|
||||
* when omitted: arbitrary C can depend on same-path edited headers and on
|
||||
* compiler-visible source spelling (`__FILE__`), neither of which the
|
||||
* top-level bytes alone can safely represent. scriptc's frontend supplies
|
||||
* this for its generated C/LLVM IR; `--from-c` deliberately does not. */
|
||||
* this for its generated LLVM IR; caller-supplied C deliberately does not. */
|
||||
cacheIdentity?: string;
|
||||
/** Native optimization posture. Release preserves the historical -O2
|
||||
* executable lane; dev selects -O0 and may compile a caller-provided LLVM
|
||||
@@ -1001,11 +1001,11 @@ export interface LibArchiveOptions {
|
||||
/** Canonical externally visible definitions retained while the shard merge
|
||||
* demotes generated cross-shard linkage back to local symbols. */
|
||||
programPublicSymbols?: readonly string[];
|
||||
/** Tiny generated C source carrying volatile library identity getters.
|
||||
/** Tiny LLVM module carrying volatile library identity getters.
|
||||
* Its bytes join the complete archive key, but the source itself exists
|
||||
* only in the invocation-private build directory and the large program-
|
||||
* object cache is keyed independently. */
|
||||
identityCSource?: string;
|
||||
identityLlvmSource?: string;
|
||||
/** The archive to produce (<name>.lib.a). */
|
||||
outPath: string;
|
||||
/** Caller-owned identity for the generated TU's complete non-system
|
||||
@@ -1284,9 +1284,9 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
let cachedProgramBytes = opts.programSource === undefined
|
||||
? null
|
||||
: Buffer.from(opts.programSource, "utf8");
|
||||
const identityBytes = opts.identityCSource === undefined
|
||||
const identityBytes = opts.identityLlvmSource === undefined
|
||||
? null
|
||||
: Buffer.from(opts.identityCSource, "utf8");
|
||||
: Buffer.from(opts.identityLlvmSource, "utf8");
|
||||
if (persistentCache !== null) {
|
||||
try {
|
||||
const [cv, fingerprint, programBytes] = await Promise.all([
|
||||
@@ -1639,7 +1639,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
const identityObject = identityBytes === null
|
||||
? null
|
||||
: await (async () => {
|
||||
const source = join(buildDir, "identity.c");
|
||||
const source = join(buildDir, "identity.ll");
|
||||
await writeFile(source, identityBytes);
|
||||
return compileOne(source, `${stem}.identity.o`);
|
||||
})();
|
||||
@@ -1848,7 +1848,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
* mirrors MSVC link.exe; zig's COFF driver refuses multi-object
|
||||
* merges) and llvm-objcopy rejects symbol-scope flags for
|
||||
* COFF, so no tool pairing exists to shell out to. */
|
||||
async function localizeLibraryObjects(
|
||||
export async function localizeLibraryObjects(
|
||||
driver: CcDriver,
|
||||
arArgv: readonly string[],
|
||||
buildDir: string,
|
||||
@@ -1856,8 +1856,8 @@ async function localizeLibraryObjects(
|
||||
supportObjects: readonly string[],
|
||||
keepSymbols: readonly string[],
|
||||
stem: string,
|
||||
platform = targetPlatform(driver),
|
||||
): Promise<string> {
|
||||
const platform = targetPlatform(driver);
|
||||
const combined = join(buildDir, `${stem}.localized.o`);
|
||||
const staging = join(buildDir, `${stem}.localize-staging.a`);
|
||||
const keepFile = join(buildDir, "localize-keep.syms");
|
||||
@@ -1949,9 +1949,9 @@ async function localizeLibraryObjects(
|
||||
* (every .c/.h in the runtime src dir plus the vendor pin
|
||||
* QJS_COMMIT), the caller's dependency identity, the
|
||||
* compiler-visible TU path, Darwin output basename, the
|
||||
* FULL normalized command line, and the emitted C bytes).
|
||||
* Emitted C is
|
||||
* byte-stable by project invariant, so unchanged programs
|
||||
* FULL normalized command line, and the emitted LLVM bytes).
|
||||
* Emitted LLVM is
|
||||
* deterministic, so unchanged programs
|
||||
* hit; any flag difference — e.g. the sanitized lane's
|
||||
* -O1/-fsanitize=address/-DSCR_RC_AUDIT — lands in a
|
||||
* naturally distinct key. On a hit the cached binary is
|
||||
@@ -1980,7 +1980,7 @@ async function localizeLibraryObjects(
|
||||
* obj/<set>/<f>.o — per-flavor runtime objects for cache-miss builds. The
|
||||
* historical single invocation recompiles every runtime
|
||||
* TU per program (~1.3s at -O2); with cached objects a
|
||||
* miss compiles ONLY the program's C and links (~0.15s).
|
||||
* miss compiles only the program's LLVM and links (~0.15s).
|
||||
* Library-mode -DSCR_LIB objects use a distinct flavor.
|
||||
* The clang driver hands every input the same option set,
|
||||
* so per-TU `-c` compiles with those options plus a final
|
||||
@@ -4133,7 +4133,7 @@ async function compileCInternal(
|
||||
cachePolicy.runtimeObjects &&
|
||||
configuredCacheRoot !== null &&
|
||||
await compilerDriverSupportsPersistentCache(driver, toolchainEnv);
|
||||
// Only compiler-generated TUs opt in. Arbitrary `compileC` / `--from-c`
|
||||
// Only compiler-generated TUs opt in. Arbitrary `compileC` inputs
|
||||
// inputs may include caller-owned headers whose contents are not otherwise
|
||||
// represented in this key, so they retain the fully uncached historical
|
||||
// path unless the caller supplies its own complete dependency identity.
|
||||
@@ -4452,15 +4452,15 @@ async function compileCInternal(
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
"-fno-math-errno",
|
||||
// The emitted object model is deliberately type-punned C: a hierarchy
|
||||
// The runtime object model uses type-punned C: a hierarchy
|
||||
// upcast is a raw pointer cast, so one object's header (rc, vt) and
|
||||
// fields are read and written through BOTH the base and derived struct
|
||||
// types (sc_retain_Derived vs sc_release_Base on the same object).
|
||||
// C's effective-type rule calls that UB, and clang's TBAA at -O2
|
||||
// reorders/elides the rc updates once everything inlines — an upcast
|
||||
// identity compare frees the object while a global still owns it.
|
||||
// The LLVM backend emits no TBAA metadata, so this flag is also what
|
||||
// keeps the two backends' memory semantics identical. Mirrored in the
|
||||
// The LLVM backend emits no TBAA metadata; this flag preserves
|
||||
// matching memory semantics in the runtime. Mirrored in the
|
||||
// cache-miss cflags below and compileLibArchive — the three option
|
||||
// sets must stay in lockstep.
|
||||
"-fno-strict-aliasing",
|
||||
@@ -4671,8 +4671,7 @@ async function compileCInternal(
|
||||
(opts.systemLibraries?.length ?? 0) > 0
|
||||
? "This build includes native FFI link inputs. Check that every symbol and system library exists, " +
|
||||
"that archive/object ordering is correct, and that each input matches the selected target."
|
||||
: `This is a scriptc bug (generated C should always compile) unless ` +
|
||||
`${ccName} itself is missing/broken.`;
|
||||
: `Check the supplied native source and the selected ${ccName} toolchain.`;
|
||||
throw new CcCompileError(
|
||||
ccName,
|
||||
stderr,
|
||||
|
||||
@@ -611,6 +611,28 @@ describe("mergeAndLocalizeCoffObjects", () => {
|
||||
expect(stubs[0]!.storageClass).toBe(IMAGE_SYM_CLASS_STATIC);
|
||||
});
|
||||
|
||||
test("private COMDAT code and leaderless unwind sections retain each object's relocations", () => {
|
||||
const member = (name: string, marker: number): Uint8Array => buildCoff([
|
||||
{ name: ".text", data: new Uint8Array([marker]), characteristics: 0x60000020 | IMAGE_SCN_LNK_COMDAT, comdatSelection: 1 },
|
||||
{ name: ".xdata", data: new Uint8Array([marker + 10]), characteristics: 0x40000040 | IMAGE_SCN_LNK_COMDAT, comdatSelection: 2 },
|
||||
{ name: ".pdata", data: new Uint8Array(8), relocs: [{ va: 0, sym: 2, type: 3 }, { va: 4, sym: 3, type: 3 }] },
|
||||
], [
|
||||
{ name: ".text", section: 1, storageClass: IMAGE_SYM_CLASS_STATIC, sectionDef: true },
|
||||
{ name: "private_fn", section: 1, storageClass: IMAGE_SYM_CLASS_STATIC },
|
||||
{ name: ".xdata", section: 2, storageClass: IMAGE_SYM_CLASS_STATIC, sectionDef: true },
|
||||
{ name: ".pdata", section: 3, storageClass: IMAGE_SYM_CLASS_STATIC, sectionDef: true },
|
||||
{ name, section: 1 },
|
||||
]);
|
||||
const merged = readCoff(mergeAndLocalizeCoffObjects([member("first", 1), member("second", 2)], [], new Set(["first", "second"])));
|
||||
expect(merged.sections).toHaveLength(6);
|
||||
expect(merged.sections.filter((s) => s.name === ".text").map((s) => [...s.data])).toEqual([[1], [2]]);
|
||||
expect(merged.sections.filter((s) => s.name === ".xdata").map((s) => [...s.data])).toEqual([[11], [12]]);
|
||||
for (const section of merged.sections) expect(section.characteristics & IMAGE_SCN_LNK_COMDAT).toBe(0);
|
||||
const byIndex = new Map(merged.symbols.map((s) => [s.index, s]));
|
||||
const pdata = merged.sections.filter((s) => s.name === ".pdata");
|
||||
expect(pdata.map((s) => s.relocs.map((r) => byIndex.get(r.sym)?.section))).toEqual([[1, 2], [4, 5]]);
|
||||
});
|
||||
|
||||
const comdatPair = (
|
||||
selection: number,
|
||||
firstData: Uint8Array,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user