diff --git a/.dockerignore b/.dockerignore index f67babefc..8d8d8d713 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e23d2c4d7..d311c3b46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e202485bc..89f18cc60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index e99348b3f..e2af7855a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index 6192f52d4..f0db1c9e5 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -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 diff --git a/README.md b/README.md index c33f6b3f9..2c8d849e0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ npm downloads per month: scriptc

-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-` and `@scriptc/runtime-` `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. - -`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. +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-` and `@scriptc/runtime-` `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. + +`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. diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index 5d9bceb8a..c94dfd1ac 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -13,13 +13,13 @@ Usage: scriptc coverage --dynamic what a --dynamic build compiles, and what still blocks it scriptc coverage --external-types 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 wasm32-wasi 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 wasm32-wasi 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 scr_* runtime symbols and a required scr_runtime_abi_v4 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed source runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe 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 scr_* runtime symbols and a required scr_runtime_abi_v4 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe 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
-o, --out <path>
-
Primary artifact path. An explicit path is exact. Defaults are .scriptc/<name>.ir.json, .c, .ll, .s, .o, or the platform executable name.
+
Primary artifact path. An explicit path is exact. Defaults are .scriptc/<name>.ir.json, .ll, .s, .o, or the platform executable name.
-
--emit <ir|c|llvm|asm|obj|exe>
-
Select the invocation's one primary artifact. ir, c, and llvm need only Node. asm and obj use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. exe is the default.
+
--emit <ir|llvm|asm|obj|exe>
+
Select the invocation's one primary artifact. ir and llvm need only Node. asm and obj use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. exe is the default.
--print <native-link-info>
-
Build an object (equivalent to --emit=obj) 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.
+
Build an object (equivalent to --emit=obj) 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.
--dynamic
Embed the dynamic engine (~620KB) so npm dependencies and any-typed code can run. Static stays the default — without this flag, dynamic-tier sites are per-site compile errors. See npm Dependencies.
@@ -98,17 +91,17 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
--ffi <file>
Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See Native FFI.
-
--backend <c|llvm>
-
Code generator: llvm (default) or c (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 wasm32-wasi target never falls back: a missing LLVM lowering is SC3001.
+
--backend <llvm>
+
The production code generator is llvm. This is also the default when the option is omitted.
--strip
-
Remove symbol and debug payload when linking an executable, including cross-compiled targets. This opt-in flag works with either backend and --from-c without changing code optimization.
+
Remove symbol and debug payload when linking an executable, including cross-compiled targets. This flag preserves code optimization.
--optimization <release|dev>
Select native optimization for executable, assembly, or object output. The default is release (-O2). dev uses -O0 and emits source locations for TypeScript and JavaScript breakpoints and stack frames. LLVM builds include source variable names and native storage descriptions; see Native debugging for the supported representations. macOS executable builds also produce an adjacent .dSYM bundle.
--windows-subsystem <console|gui>
-
Select the subsystem of a Windows executable. console is the default; gui prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows, including --from-c.
+
Select the subsystem of a Windows executable. console is the default; gui prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows.
--npm-static <pkg[,pkg…]|auto>
EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; auto opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See npm Dependencies for maturity notes.
@@ -125,11 +118,8 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
--emit-ir
Additive IR side artifact. It is deprecated for executable builds for one release cycle; prefer --emit=ir when IR is the primary output. Library mode retains --emit-ir because --emit does not select library artifacts.
-
--keep-c / --no-keep-c
-
Keep (default) or delete the generated program translation unit next to the executable — the .ll file under the default LLVM backend, or the .c under --backend c (and when the default build fell back). The generated C is readable and annotated with source lines.
- -
--from-c
-
Treat the input as a C (or .ll) file. Toolchain plumbing and debugging. These arbitrary translation units bypass the persistent artifact cache because their header dependency graph is caller-owned.
+
--keep-llvm / --no-keep-llvm
+
Keep (default) or delete the generated .ll file next to the executable.
-h, --help
Show usage.
@@ -139,7 +129,7 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
SCRIPTC_CACHE_DIR
-
Override the persistent build-cache root. By default it is $XDG_CACHE_HOME/scriptc/build when that variable is set, $HOME/Library/Caches/scriptc/build on macOS, %LOCALAPPDATA%\scriptc\cache\build on Windows, and $HOME/.cache/scriptc/build 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 system_libraries always relink against their current dependencies while retaining runtime-object reuse. Mutable compiler input paths such as CPATH and SDKROOT, 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, zig cc, trusted platform archivers, and zig ar retain their applicable persistent tiers. An existing POSIX override must already be private; otherwise caching is bypassed without changing the directory's permissions.
+
Override the persistent build-cache root. By default it is $XDG_CACHE_HOME/scriptc/build when set, $HOME/Library/Caches/scriptc/build on macOS, %LOCALAPPDATA%\scriptc\cache\build on Windows, and $HOME/.cache/scriptc/build 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.
SCRIPTC_NO_CACHE
Set to 1 to bypass all build-cache reads and writes. An explicitly empty SCRIPTC_CACHE_DIR has the same effect.
@@ -148,38 +138,26 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
Maximum build-cache size in megabytes. The default is 4096; 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.
SCRIPTC_CC
-
The C compiler for explicit C builds, sanitizer builds, the temporary LLVM-fallback path, and cross-compilation. zigcc selects zig's bundled clang and enables cross-compilation with its bundled sysroots. On macOS arm64, setting SCRIPTC_CC=clang|zigcc selects a deprecated legacy executable path; leave it unset for the bundled helper/runtime-pack route.
+
The external compiler for runtime development with AddressSanitizer and native FFI build tooling. zigcc selects Zig's cc subcommand.
SCRIPTC_LINKER
-
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.
+
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.
SCRIPTC_TARGET
Target triple for cross-compilation, e.g. aarch64-linux-gnu.2.36, x86_64-windows-gnu, or wasm32-wasi. WASI builds default to a .wasm output name. See Platform Support.
```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 - -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; SCRIPTC_LINKER 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 wasm32-wasi 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. +## LLVM backend -For source breakpoints in Xcode or LLDB, build with --optimization=dev. In an Xcode custom build rule, add this option to the scriptc build invocation and list both the executable and its .dSYM 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. +scriptc emits LLVM IR and lowers it to native code. Executable builds link the resulting object with the matching precompiled C runtime pack. Use --emit=llvm to inspect generated code. -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 --backend llvm 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 --optimization=dev. In an Xcode custom build rule, list the executable and its .dSYM 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 --backend llvm pins the LLVM backend and fails with dia - coverage, --emit=ir|c|llvm + coverage, --emit=ir|llvm Required Not used Not used @@ -206,22 +184,16 @@ An explicit --backend llvm pins the LLVM backend and fails with dia Not used - External link of --emit=obj with the reported source runtime pack + External link of --emit=obj with the reported precompiled runtime pack Not used by the artifact - C compiler required for runtime sources + Precompiled runtime objects macOS linker and SDK required --emit=exe Required to run scriptc - clang or the selected cross compiler - Required by that compiler driver - - - --from-c - Required to run scriptc - Required - Required + Bundled LLVM helper + Platform linker and SDK/sysroot diff --git a/docs/src/app/how-it-works/page.mdx b/docs/src/app/how-it-works/page.mdx index c42e4a5c6..000db9e06 100644 --- a/docs/src/app/how-it-works/page.mdx +++ b/docs/src/app/how-it-works/page.mdx @@ -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 wasm32-wasi 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 main and leave their selected scr_* runtime functions undefined. The scr_runtime_abi_v4 reference is a strong link-time compatibility check. --print=native-link-info 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 main and leave their selected scr_* runtime functions undefined. The scr_runtime_abi_v4 reference is a strong link-time compatibility check. --print=native-link-info 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 packages/compiler - The frontend (tsc API → IR), the typed IR with validator and serializer, the C and LLVM backends, native-helper integration, and the coverage analyzer. + The frontend (tsc API → IR), the typed IR with validator and serializer, the LLVM backend, native-helper integration, and the coverage analyzer. native/llvm-codegen diff --git a/docs/src/app/limitations/page.mdx b/docs/src/app/limitations/page.mdx index 4e8291a42..f8f805142 100644 --- a/docs/src/app/limitations/page.mdx +++ b/docs/src/app/limitations/page.mdx @@ -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) diff --git a/docs/src/app/native-objects/page.mdx b/docs/src/app/native-objects/page.mdx index 89dff9b62..38a52decf 100644 --- a/docs/src/app/native-objects/page.mdx +++ b/docs/src/app/native-objects/page.mdx @@ -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. diff --git a/docs/src/app/platforms/page.mdx b/docs/src/app/platforms/page.mdx index 25d4e8bb0..89b4797cd 100644 --- a/docs/src/app/platforms/page.mdx +++ b/docs/src/app/platforms/page.mdx @@ -2,15 +2,13 @@ ## Owned LLVM targets -Source artifacts selected with --emit=ir|c|llvm need only Node. On supported hosts, --emit=asm|obj 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 --sanitize still require a C compiler. Object output retains undefined runtime references and is not a library archive. +Source artifacts selected with --emit=ir|llvm need only Node. On supported hosts, --emit=asm|obj 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 --sanitize 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:
-
SCRIPTC_CC=zigcc
-
Use zig's clang as the C compiler.
SCRIPTC_TARGET=<triple>
The target triple; GNU/Linux targets include a glibc version.
@@ -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 --windows-subsystem=gui 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 --windows-subsystem=gui 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
SCRIPTC_TARGET=aarch64-apple-ios
-
iOS device archives (Mach-O, arm64). Requires a macOS host with Xcode's iPhoneOS SDK. Every object carries LC_BUILD_VERSION with minimum OS 15.0.
+
iOS device archives (Mach-O, arm64). Requires a macOS host. The runtime pack was built with Xcode's iPhoneOS SDK. Every object carries LC_BUILD_VERSION with minimum OS 15.0.
SCRIPTC_TARGET=aarch64-apple-ios-simulator
-
iOS simulator archives (Mach-O, arm64, simulator platform). Requires a macOS host with Xcode's iPhoneSimulator SDK. Same iOS 15.0 minimum.
+
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.
SCRIPTC_TARGET=aarch64-linux-android
-
Android archives (ELF, arm64) compiled against the NDK's bionic headers at API level 26 — the embedder's minSdkVersion must be 26 or higher. Requires an Android NDK: ANDROID_NDK_ROOT, or the newest ndk/<version> under ANDROID_HOME or the default SDK location.
+
Android archives (ELF, arm64) compiled against the NDK's bionic headers at API level 26 — the embedder's minSdkVersion must be 26 or higher. Creating the archive uses the precompiled Android runtime pack; the embedding app uses the NDK for its final link.
```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 SCRIPTC_CC=zigcc and SCRIPTC_TARGET=wasm32-wasi to produce a standalone .wasm module. Without -o, scriptc build hello.ts writes .scriptc/hello.wasm. scriptc run hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as /, and maps the host platform's temporary directory to the guest's /tmp. A built module can run in another WASI Preview 1 host instead. +Set SCRIPTC_TARGET=wasm32-wasi to produce a standalone .wasm module. Without -o, scriptc build hello.ts writes .scriptc/hello.wasm. scriptc run hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as /, and maps the host platform's temporary directory to the guest's /tmp. 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 --dynamic QuickJS island. The debugging C backend remains available for async-free inspection only through an explicit --backend c; coroutine-dependent programs report SC3001, 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 --dynamic 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, os.networkInterfaces(), and fs.watch therefore fail before linking with diagnostic SC3002. --sanitize, 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; wasm32-wasi rejects scriptc build --lib with SC3002. - Mobile triples are library-mode-only: standalone executable builds reject with SC3002, 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 Windows x86_64 native x64 MSVC helper + runtime pack - Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; executable linking needs the Windows SDK/MSVC CRT + Static and dynamic surfaces incl. servers, TLS, fetch, and child_process; executable linking uses Zig’s Windows sysroot iOS arm64 (device and simulator) - SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-apple-ios or aarch64-apple-ios-simulator, macOS host with Xcode + SCRIPTC_TARGET=aarch64-apple-ios or aarch64-apple-ios-simulator, macOS host with Xcode Library mode only (--lib): static archives for Xcode projects, iOS 15.0 minimum; multi-instance and thread-instanced profiles; simulator-executed test matrix Android arm64 - SCRIPTC_CC=zigcc SCRIPTC_TARGET=aarch64-linux-android, any host with an NDK + SCRIPTC_TARGET=aarch64-linux-android, any supported host with Zig Library mode only (--lib): static archives for Gradle/NDK projects, API level 26 minimum; multi-instance and thread-instanced profiles; emulator-executed test matrix diff --git a/examples/native-object/README.md b/examples/native-object/README.md index 0dad939fe..392e9190c 100644 --- a/examples/native-object/README.md +++ b/examples/native-object/README.md @@ -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. diff --git a/examples/native-object/link.mjs b/examples/native-object/link.mjs index cba5962e7..e467fa18d 100644 --- a/examples/native-object/link.mjs +++ b/examples/native-object/link.mjs @@ -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); } } diff --git a/native/llvm-codegen/CMakeLists.txt b/native/llvm-codegen/CMakeLists.txt index 09c8bc681..2fa51afa9 100644 --- a/native/llvm-codegen/CMakeLists.txt +++ b/native/llvm-codegen/CMakeLists.txt @@ -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") diff --git a/native/llvm-codegen/src/target.cpp b/native/llvm-codegen/src/target.cpp index c51621919..5d984acf2 100644 --- a/native/llvm-codegen/src/target.cpp +++ b/native/llvm-codegen/src/target.cpp @@ -118,6 +118,9 @@ std::unique_ptr 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(Definition->createTargetMachine( TargetTriple, "generic", "", Options, Reloc::PIC_, CodeModel::Small, codeGenLevel(OptLevel))); diff --git a/native/llvm-codegen/targets.mjs b/native/llvm-codegen/targets.mjs new file mode 100644 index 000000000..e34bd0aa8 --- /dev/null +++ b/native/llvm-codegen/targets.mjs @@ -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", +]; diff --git a/packages/cli/README.md b/packages/cli/README.md index bc6aef62f..89ad44ddc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -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 ` — compile and run - `scriptc coverage ` — 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 ` 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 ` 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 `. See the [Native FFI guide](https://scriptc.dev/ffi). diff --git a/packages/cli/package.json b/packages/cli/package.json index 1a109b25f..a3e23250f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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:*" diff --git a/packages/cli/scripts/runtime-pack-host.d.mts b/packages/cli/scripts/runtime-pack-host.d.mts deleted file mode 100644 index 1a7b53d29..000000000 --- a/packages/cli/scripts/runtime-pack-host.d.mts +++ /dev/null @@ -1,5 +0,0 @@ -export function hostSupportsRuntimePack( - platform?: NodeJS.Platform, - architecture?: string, - hostRelease?: string, -): boolean; diff --git a/packages/cli/scripts/runtime-pack-host.mjs b/packages/cli/scripts/runtime-pack-host.mjs deleted file mode 100644 index 6e8933e22..000000000 --- a/packages/cli/scripts/runtime-pack-host.mjs +++ /dev/null @@ -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"); -} diff --git a/packages/cli/scripts/warm-cache.mjs b/packages/cli/scripts/warm-cache.mjs deleted file mode 100644 index c22eb186b..000000000 --- a/packages/cli/scripts/warm-cache.mjs +++ /dev/null @@ -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`); - } - } -} diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 82cbaf72c..bd509f49a 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -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 { (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 { // 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 { 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 { 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; diff --git a/packages/cli/src/legacy-c-warning.test.ts b/packages/cli/src/legacy-c-warning.test.ts deleted file mode 100644 index cf858c486..000000000 --- a/packages/cli/src/legacy-c-warning.test.ts +++ /dev/null @@ -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); -}); diff --git a/packages/cli/src/legacy-c-warning.ts b/packages/cli/src/legacy-c-warning.ts deleted file mode 100644 index 930f8be19..000000000 --- a/packages/cli/src/legacy-c-warning.ts +++ /dev/null @@ -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"] !== ""; -} diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 1820f770e..35ec1cb35 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -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 { 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 { 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 { 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 { : { 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 { } 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 => { - 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 { 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; } diff --git a/packages/cli/src/output-options.ts b/packages/cli/src/output-options.ts index 5a7eeffd3..df5fd24e8 100644 --- a/packages/cli/src/output-options.ts +++ b/packages/cli/src/output-options.ts @@ -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(["ir", "c", "llvm"]); +const SOURCE_KINDS = new Set(["ir", "llvm"]); const NATIVE_ARTIFACT_KINDS = new Set(["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, }; diff --git a/packages/cli/src/paths.ts b/packages/cli/src/paths.ts index 08e139cbd..a803e2a3c 100644 --- a/packages/cli/src/paths.ts +++ b/packages/cli/src/paths.ts @@ -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 = { ir: ".ir.json", - c: ".c", llvm: ".ll", asm: ".s", obj: ".o", diff --git a/packages/cli/src/usage.ts b/packages/cli/src/usage.ts index 82970a8a7..135aaced2 100644 --- a/packages/cli/src/usage.ts +++ b/packages/cli/src/usage.ts @@ -18,19 +18,11 @@ Usage: Options: -o, --out primary output path (default: .scriptc/) - --emit primary output: ir, c, llvm, asm, obj, or exe + --emit primary output: ir, llvm, asm, obj, or exe (default: exe). asm/obj use the matching platform helper --print print machine-readable metadata instead of the output path (native-link-info implies --emit=obj and never links) - --backend 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 code generator (llvm) --optimization native optimization posture (default: release/-O2). dev uses -O0, source breakpoints, and cached LLVM object shards; @@ -40,11 +32,8 @@ Options: --windows-subsystem 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 }, diff --git a/packages/cli/test/bootstrap.test.ts b/packages/cli/test/bootstrap.test.ts index 8b6d31e73..b1de15461 100644 --- a/packages/cli/test/bootstrap.test.ts +++ b/packages/cli/test/bootstrap.test.ts @@ -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 }); diff --git a/packages/cli/test/cache-warm.test.ts b/packages/cli/test/cache-warm.test.ts index b427284ec..21f6ee5e9 100644 --- a/packages/cli/test/cache-warm.test.ts +++ b/packages/cli/test/cache-warm.test.ts @@ -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 }); } diff --git a/packages/cli/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index 6127d0370..18aa30d83 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -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 }); } diff --git a/packages/cli/test/library-output.test.ts b/packages/cli/test/library-output.test.ts index b9dd097ce..4fab0bde7 100644 --- a/packages/cli/test/library-output.test.ts +++ b/packages/cli/test/library-output.test.ts @@ -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 `.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(); diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 95a01fc58..b75b607bf 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -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, ]); diff --git a/packages/cli/test/output-options.test.ts b/packages/cli/test/output-options.test.ts index b65079207..4e8f933ec 100644 --- a/packages/cli/test/output-options.test.ts +++ b/packages/cli/test/output-options.test.ts @@ -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", diff --git a/packages/cli/test/paths.test.ts b/packages/cli/test/paths.test.ts index 0e89a1137..23f339b60 100644 --- a/packages/cli/test/paths.test.ts +++ b/packages/cli/test/paths.test.ts @@ -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[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[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", () => { diff --git a/packages/cli/test/runtime-pack-host.test.ts b/packages/cli/test/runtime-pack-host.test.ts deleted file mode 100644 index 8439652a5..000000000 --- a/packages/cli/test/runtime-pack-host.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/cli/test/runtime-pack.test.ts b/packages/cli/test/runtime-pack.test.ts index 1aa24b7d9..5747b3056 100644 --- a/packages/cli/test/runtime-pack.test.ts +++ b/packages/cli/test/runtime-pack.test.ts @@ -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"); }); }); diff --git a/packages/cli/test/source-output.test.ts b/packages/cli/test/source-output.test.ts index 2a8cfaf39..2d9890bbc 100644 --- a/packages/cli/test/source-output.test.ts +++ b/packages/cli/test/source-output.test.ts @@ -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"), }); diff --git a/packages/cli/test/strip.test.ts b/packages/cli/test/strip.test.ts index a4fe6d5f3..6c4fa4724 100644 --- a/packages/cli/test/strip.test.ts +++ b/packages/cli/test/strip.test.ts @@ -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 => { 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 \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"); diff --git a/packages/cli/test/wasi-runtime-pack.test.ts b/packages/cli/test/wasi-runtime-pack.test.ts index 653ce57eb..1a36bfbf1 100644 --- a/packages/cli/test/wasi-runtime-pack.test.ts +++ b/packages/cli/test/wasi-runtime-pack.test.ts @@ -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); diff --git a/packages/cli/test/windows-subsystem.test.ts b/packages/cli/test/windows-subsystem.test.ts index 511e63243..ecdb38466 100644 --- a/packages/cli/test/windows-subsystem.test.ts +++ b/packages/cli/test/windows-subsystem.test.ts @@ -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, diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 22fa735f0..b77894ff4 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -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:*" } } diff --git a/packages/compiler/src/backend/c/async.ts b/packages/compiler/src/backend/c/async.ts deleted file mode 100644 index 1f221067b..000000000 --- a/packages/compiler/src/backend/c/async.ts +++ /dev/null @@ -1,1556 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; -/* Async C emission: the per-async-function argpack/trampoline/spawn-wrapper - * scaffolding, plus the interned resolve/child-exit thunks that adapt typed - * payloads onto the runtime's promise and child-process machinery. */ -import type { CEmitter } from "./c-emitter.js"; -import { mangleArgPack, mangleAsyncSpawn, mangleChildDataThunk, mangleChildExitThunk, mangleCloseBindThunk, mangleCloseOverrideWrap, mangleConnectResThunk, mangleConnectSockThunk, mangleDgramMsgThunk, mangleDnsLookupThunk, mangleField, mangleFsRenameThunk, mangleFunction, mangleGenDrop, mangleGenResThunk, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRaceThunk, mangleRawParam, mangleNetLookupAnswerThunk, mangleEmitterInvokeThunk, mangleStreamCbThunk, mangleStreamDoneFn, mangleRecordNew, mangleRecordRelease, mangleRecordStruct, mangleResolveThunk, mangleSniAnswerThunk, mangleTrampoline } from "../mangle.js"; -import { cDecl, cType, releaseCallC, retainCallC, vAdapters } from "./types.js"; -import { type IrFunction, type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; - -interface ArgPackAndTrampolinePrologue { - definitions: string[]; - pack: string; - lifted: boolean; - pname: (p: IrFunction["params"][number]) => string; - ret: IrType; - lines: string[]; - spawnParams: string[]; - argPackLines: string[]; -} - -/** The argument-pack ABI and trampoline prefix shared by async functions - * and generators. Their promise/generator completion and spawn tails stay - * with the callers below. */ -function emitArgPackAndTrampolinePrologue( - fn: IrFunction, -): ArgPackAndTrampolinePrologue { - const pack = mangleArgPack(fn.name); - const lifted = fn.captures !== undefined; - const fields: string[] = []; - if (lifted) fields.push("ScrClosure *sc_env;"); - const boxedIds = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id)); - const pname = (p: IrFunction["params"][number]) => - boxedIds.has(p.localId) ? mangleRawParam(p.localId) : mangleLocal(p.localId); - for (const p of fn.params) fields.push(`${cDecl(p.type, pname(p))};`); - const definitions = [``, `typedef struct { ${fields.join(" ") || "char sc_unused;"} } ${pack};`]; - - const callArgs = [ - ...(lifted ? ["sc_a.sc_env"] : []), - ...fn.params.map((p) => `sc_a.${pname(p)}`), - ].join(", "); - const ret = fn.returnType; - const bodyCall = `${mangleFunction(fn.name)}(${callArgs})`; - const lines: string[] = [ - `static void ${mangleTrampoline(fn.name)}(ScrFiber *sc_self, void *sc_ap0) {`, - ` ${pack} sc_a = *(${pack} *)sc_ap0;`, - ` free(sc_ap0);`, - ]; - if (ret.kind === "void") { - lines.push(` ${bodyCall};`); - } else { - lines.push(` ${cDecl(ret, "sc_r")} = ${bodyCall};`); - } - if (lifted) lines.push(` scr_closure_release(sc_a.sc_env);`); - - const spawnParams = [ - ...(lifted ? ["ScrClosure *sc_env"] : []), - ...fn.params.map((p) => cDecl(p.type, pname(p))), - ]; - const argPackLines = [ - ` ${pack} *sc_ap = malloc(sizeof *sc_ap);`, - ` if (!sc_ap) { scr_trap("scriptc: out of memory\\n"); }`, - ...(lifted ? [` sc_ap->sc_env = scr_closure_retain(sc_env);`] : []), - ...fn.params.map((p) => ` sc_ap->${pname(p)} = ${pname(p)};`), - ]; - return { definitions, pack, lifted, pname, ret, lines, spawnParams, argPackLines }; -} - -/** Per-async-function machinery: an argument pack, a fiber trampoline - * (unpacks, runs the ordinary compiled body, settles the promise), and a - * spawn wrapper call sites use. Lifted async lambdas additionally thread - * their closure env through the pack (+1, released after the body). */ - export function emitAsyncScaffolding(emitter: CEmitter, out: string[]): void { - for (const fn of emitter.mod.functions) { - if (!fn.async || fn.generator !== undefined) continue; - const { definitions, ret, lines, spawnParams, argPackLines } = - emitArgPackAndTrampolinePrologue(fn); - out.push(...definitions); - lines.push(` if (!scr_exc_pending()) {`); - switch (ret.kind) { - case "void": - lines.push(` scr_promise_fulfill_void(scr_fiber_promise(sc_self));`); - break; - case "f64": - case "date": - lines.push(` scr_promise_fulfill_f64(scr_fiber_promise(sc_self), sc_r);`); - break; - case "bool": - lines.push(` scr_promise_fulfill_bool(scr_fiber_promise(sc_self), sc_r);`); - break; - case "string": - lines.push(` scr_promise_fulfill_str(scr_fiber_promise(sc_self), sc_r);`); - break; - default: { - const v = vAdapters(ret); - lines.push( - ` scr_promise_fulfill_ref(scr_fiber_promise(sc_self), sc_r, ${v.retain}, ${v.release}, ${emitter.traceArgC(ret)});`, - ); - } - } - lines.push(` }`); - if (ret.kind !== "void" && isRefCounted(ret)) { - // An escaping throw means sc_r is the never-read dummy (NULL). - lines.push(` else { ${releaseCallC(ret, "sc_r")}; }`); - } - lines.push(`}`); - out.push(...lines); - - const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null; - const cycleCache = - fn.asyncCycleCacheGlobal !== undefined ? mangleGlobal(fn.asyncCycleCacheGlobal) : null; - out.push( - `static ScrPromise *${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ") || "void"}) {`, - ...(cache !== null - ? [` if (${cache}) return scr_promise_retain(${cache});`] - : []), - ...argPackLines, - ` ScrPromise *sc_p = scr_async_spawn(&${mangleTrampoline(fn.name)}, sc_ap);`, - ...(cache !== null - ? [ - // Module evaluation promises are loader-owned from the - // moment evaluation starts. Mark even a pending promise - // handled before a later sibling initializer can throw and - // unwind past the dependency wait. - ` scr_promise_mark_handled(sc_p);`, - ] - : []), - ...(cache !== null - ? [ - // scr_async_spawn runs eagerly. An admitted async import - // cycle can therefore re-enter this initializer after its - // guard sets but before this outer spawn returns; that - // guarded inner spawn temporarily fills the cache. Replace - // it ownership-safely when the real evaluation promise - // arrives. - ` ScrPromise *sc_cache_owned = scr_promise_retain(sc_p);`, - ` scr_promise_release(${cache});`, - ` ${cache} = sc_cache_owned;`, - ] - : []), - ...(cycleCache !== null - ? [ - // Every member of an async SCC publishes while eager spawn - // recursion unwinds. The outermost wrapper writes last, so - // this records the member that actually rooted evaluation. - ` ScrPromise *sc_cycle_cache_owned = scr_promise_retain(sc_p);`, - ` scr_promise_release(${cycleCache});`, - ` ${cycleCache} = sc_cycle_cache_owned;`, - ] - : []), - ` return sc_p;`, - `}`, - ); - } - emitGenScaffolding(emitter, out); - } - -/** Per-generator-function machinery — the async scaffolding's lazy - * sibling: the same argument pack, a fiber trampoline whose epilogue - * stores the COMPLETION value (or consumes the GENRET sentinel, - * promoting the parked .return value), a spawn wrapper that only - * ALLOCATES the suspended fiber (nothing runs until the first .next()), - * and the never-started teardown that drops the packed (+1) arguments. */ - function emitGenScaffolding(emitter: CEmitter, out: string[]): void { - for (const fn of emitter.mod.functions) { - if (!fn.generator) continue; - const { definitions, pack, lifted, pname, ret, lines, spawnParams, argPackLines } = - emitArgPackAndTrampolinePrologue(fn); - out.push(...definitions); - lines.push(` ScrGen *sc_g = scr_gen_of_fiber(sc_self);`); - // Normal completion stores the (typed) return value; void completes - // with the NONE slot — JS's undefined done-value. A GENRET unwind - // consumes the sentinel and promotes the parked .return value; a - // real exception stays pending (the consumer-side resume moves it). - const store = (() => { - switch (ret.kind) { - case "void": - return null; - case "f64": - case "date": - return `scr_gen_out_f64(sc_g, sc_r);`; - case "bool": - return `scr_gen_out_bool(sc_g, sc_r);`; - default: { - const v = vAdapters(ret); - return `scr_gen_out_ref(sc_g, sc_r, ${v.release});`; - } - } - })(); - lines.push( - ` if (!scr_exc_pending()) {`, - ...(store ? [` ${store}`] : [` /* void body: the done value is undefined (NONE) */`]), - ` } else {`, - ` if (scr_exc_genret_pending()) { scr_exc_clear(); scr_gen_ret_to_out(sc_g); }`, - ...(ret.kind !== "void" && isRefCounted(ret) - ? [` ${releaseCallC(ret, "sc_r")}; /* unwound: the never-read dummy */`] - : []), - ` }`, - `}`, - ); - out.push(...lines); - - let settleAsync: string | null = null; - if (fn.async) { - const genT: IrType & { kind: "generator" } = { - kind: "generator", - async: true, - yieldT: fn.generator.yieldT, - retT: ret, - nextT: fn.generator.nextT, - }; - const resultT = fn.generator.resultType; - const build = genResultThunkFor(emitter, genT, resultT); - settleAsync = `${build}_async`; - const settleKey = `async:${typeKey(genT)}`; - if (!emitter.genResThunks.has(settleKey)) { - emitter.genResThunks.set(settleKey, settleAsync); - const adapters = vAdapters(resultT); - out.push(`static void ${settleAsync}(ScrGen *sc_g, ScrPromise *sc_p);`); - emitter.walkerDefs.push( - `static void ${settleAsync}(ScrGen *sc_g, ScrPromise *sc_p) {`, - ` ${mangleRecordStruct(resultT.shapeId)} *sc_r = ${build}(sc_g);`, - ` scr_promise_fulfill_ref(sc_p, sc_r, ${adapters.retain}, ${adapters.release}, ${emitter.traceArgC(resultT)});`, - `}`, - ``, - ); - } - } - - out.push( - `static void ${mangleGenDrop(fn.name)}(void *sc_ap0) {`, - ` ${pack} sc_a = *(${pack} *)sc_ap0;`, - ` free(sc_ap0);`, - ...(lifted ? [` scr_closure_release(sc_a.sc_env);`] : []), - ...fn.params - .filter((p) => isRefCounted(p.type)) - .map((p) => ` ${releaseCallC(p.type, `sc_a.${pname(p)}`)};`), - ...(!lifted && !fn.params.some((p) => isRefCounted(p.type)) ? [` (void)sc_a;`] : []), - `}`, - `static ScrGen *${mangleGenSpawn(fn.name)}(${spawnParams.join(", ") || "void"}) {`, - ...argPackLines, - settleAsync === null - ? ` return scr_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)});` - : ` return scr_async_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)}, &${settleAsync});`, - `}`, - ); - } - } - -/** Interned per-union child exit adapter: the runtime invokes - * adapter(cb, has_code, code, signal_name); the adapter builds the - * `number | null` union value (tags are program data — the frontend - * guaranteed the param IS that union) and calls the listener, which - * owns the union param per the universal convention (unit-arm - * instances are immortal; their release is a no-op). The one-param - * shape ignores the signal. */ - export function childExitThunkFor(emitter: CEmitter, param: IrType): string { - if (param.kind !== "union") throw new InternalCompilerError("emitter bug: exit listener param not a union"); - const key = param.unionId; - let sym = emitter.childExitThunks.get(key); - if (!sym) { - sym = mangleChildExitThunk(emitter.childExitThunks.size); - emitter.childExitThunks.set(key, sym); - const def = emitter.unionsById.get(param.unionId); - const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (f64Tag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: exit listener union lacks its arms"); - } - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, bool sc_has, double sc_code, const char *sc_sig);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, bool sc_has, double sc_code, const char *sc_sig) {`, - ` (void)sc_sig;`, - ` ScrUnion *sc_u = sc_has ? scr_union_new_f64(${f64Tag}, sc_code)`, - ` : ${emitter.unitInstanceRef(param.unionId, nullTag)};`, - ` ((void (*)(ScrClosure *, ScrUnion *))sc_cb->fn)(sc_cb, sc_u);`, - `}`, - ); - } - return sym; - } - -/** The TWO-parameter exit adapter — Node's `(code, signal)` listener: - * the code union as above plus the signal as its own `string | null` - * union (a fresh string from the runtime's static signal name when a - * signal killed the child, the null arm otherwise). */ - export function childExitSignalThunkFor(emitter: CEmitter, codeParam: IrType, sigParam: IrType): string { - if (codeParam.kind !== "union" || sigParam.kind !== "union") { - throw new InternalCompilerError("emitter bug: exit listener params not unions"); - } - const key = `${codeParam.unionId}+${sigParam.unionId}`; - let sym = emitter.childExitThunks.get(key); - if (!sym) { - sym = mangleChildExitThunk(emitter.childExitThunks.size); - emitter.childExitThunks.set(key, sym); - const codeDef = emitter.unionsById.get(codeParam.unionId); - const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1; - const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1; - const sigDef = emitter.unionsById.get(sigParam.unionId); - const strTag = sigDef ? sigDef.arms.findIndex((a) => a.kind === "string") : -1; - const sigNullTag = sigDef ? sigDef.arms.findIndex((a) => a.kind === "nullT") : -1; - if (f64Tag < 0 || codeNullTag < 0 || strTag < 0 || sigNullTag < 0) { - throw new InternalCompilerError("emitter bug: exit listener unions lack their arms"); - } - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, bool sc_has, double sc_code, const char *sc_sig);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, bool sc_has, double sc_code, const char *sc_sig) {`, - ` ScrUnion *sc_u = sc_has ? scr_union_new_f64(${f64Tag}, sc_code)`, - ` : ${emitter.unitInstanceRef(codeParam.unionId, codeNullTag)};`, - ` ScrUnion *sc_s = sc_sig`, - ` ? scr_union_new_ref(${strTag}, scr_str_new(sc_sig, strlen(sc_sig)), &scr_str_retain_v, &scr_str_release_v, NULL)`, - ` : ${emitter.unitInstanceRef(sigParam.unionId, sigNullTag)};`, - ` ((void (*)(ScrClosure *, ScrUnion *, ScrUnion *))sc_cb->fn)(sc_cb, sc_u, sc_s);`, - `}`, - ); - } - return sym; - } - -/** Interned per-union child-stream data adapter: the runtime invokes - * adapter(cb, chunk) with the chunk BORROWED; the adapter wraps a - * retained chunk at the union's Buffer arm (the frontend guaranteed - * the arm exists — the ngrok `Buffer | string` listener; strings never - * fire from a pipe) and calls the listener, which owns the union param - * per the universal convention. */ - export function childDataThunkFor(emitter: CEmitter, param: IrType): string { - if (param.kind !== "union") throw new InternalCompilerError("emitter bug: stream data listener param not a union"); - const key = param.unionId; - let sym = emitter.childDataThunks.get(key); - if (!sym) { - sym = mangleChildDataThunk(emitter.childDataThunks.size); - emitter.childDataThunks.set(key, sym); - const def = emitter.unionsById.get(param.unionId); - const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; - if (bytesTag < 0) { - throw new InternalCompilerError("emitter bug: stream data listener union lacks its Buffer arm"); - } - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_chunk);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_chunk) {`, - ` ScrUnion *sc_u = scr_union_new_ref(${bytesTag}, scr_bytes_retain(sc_chunk), &scr_bytes_retain_v, &scr_bytes_release_v, NULL);`, - ` ((void (*)(ScrClosure *, ScrUnion *))sc_cb->fn)(sc_cb, sc_u);`, - `}`, - ); - } - return sym; - } - -/** Interned per-union bound-close adapter (`server.close.bind(server)` - * as a value): invoked through the closure ABI with the callback-union - * argument, it closes the server DIRECTLY (never through the override — - * the proxy-through idiom cannot recurse), registering a present - * callback as a once-'close' listener — a ONE-param callback (Node's - * `(err?: Error) => void`) behind an emitted zero-arg trampoline that - * fires it with the undefined arm (a clean close carries no error) — - * and answers the +1 server (Node's chaining return). */ - export function closeBindThunkFor(emitter: CEmitter, cbUnion: IrType, retServer: boolean): string { - if (cbUnion.kind !== "union") throw new InternalCompilerError("emitter bug: bound-close callback param not a union"); - const key = `${cbUnion.unionId}:${retServer ? "srv" : "void"}`; - let sym = emitter.closeBindThunks.get(key); - if (sym) return sym; - sym = mangleCloseBindThunk(emitter.closeBindThunks.size); - emitter.closeBindThunks.set(key, sym); - const def = emitter.unionsById.get(cbUnion.unionId); - const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; - const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null; - if (!funcArm) throw new InternalCompilerError("emitter bug: bound-close callback union lacks its func arm"); - const oneParam = funcArm.params.length === 1; - let trampoline: string | null = null; - if (oneParam) { - const errParam = funcArm.params[0]!; - if (errParam.kind !== "union") { - throw new InternalCompilerError("emitter bug: bound-close callback's err param is not a union"); - } - const errDef = emitter.unionsById.get(errParam.unionId); - const undefTag = errDef ? errDef.arms.findIndex((a) => a.kind === "undefinedT") : -1; - if (undefTag < 0) throw new InternalCompilerError("emitter bug: bound-close err union lacks its undefined arm"); - trampoline = `${sym}_cb`; - emitter.walkerProtos.push(`static void ${trampoline}(ScrClosure *sc_self);`); - emitter.walkerDefs.push( - `static void ${trampoline}(ScrClosure *sc_self) { /* close cb: fire with no error */`, - ` ScrClosure *sc_inner = (ScrClosure *)scr_box_get_ref(sc_self->caps[0]); /* +1 */`, - ` ((void (*)(ScrClosure *, ScrUnion *))sc_inner->fn)(sc_inner, ${emitter.unitInstanceRef(errParam.unionId, undefTag)});`, - ` scr_closure_release(sc_inner);`, - `}`, - ); - } - const retC = retServer ? "ScrNetServer *" : "void"; - emitter.walkerProtos.push(`static ${retC}${retServer ? "" : " "}${sym}(ScrClosure *sc_self, ScrUnion *sc_cb);`); - emitter.walkerDefs.push( - `static ${retC}${retServer ? "" : " "}${sym}(ScrClosure *sc_self, ScrUnion *sc_cb) { /* bound server.close */`, - ` ScrNetServer *sc_srv = (ScrNetServer *)scr_box_get_ref(sc_self->caps[0]); /* +1 */`, - ` ScrClosure *sc_reg = NULL;`, - ` if (sc_cb->tag == ${funcTag}) {`, - ...(oneParam - ? [ - ` sc_reg = scr_closure_new((void *)&${trampoline}, 1);`, - ` sc_reg->caps[0] = scr_box_new(SCR_BOX_FUNC);`, - ` scr_box_set_ref(sc_reg->caps[0], scr_closure_retain((ScrClosure *)scr_union_peek(sc_cb)));`, - ] - : [` sc_reg = scr_closure_retain((ScrClosure *)scr_union_peek(sc_cb));`]), - ` }`, - ` scr_union_release(sc_cb); /* the callee owns its +1 param */`, - ` scr_net_server_close_direct(sc_srv, sc_reg); /* sc_reg moves */`, - ...(retServer - ? [` return sc_srv; /* +1 from the env read */`] - : [` scr_net_server_release(sc_srv);`]), - `}`, - ); - return sym; - } - -/** Interned close-override zero-arg wrapper (`server.close = fn`): the - * stored override must be invocable by the runtime WITHOUT program - * types, so this wrapper carries the user function and fires it with - * the undefined-arm callback argument (tags are program data), - * releasing the chaining-return server when the signature answers one. */ - export function closeOverrideWrapFor(emitter: CEmitter, cbUnion: IrType, retServer: boolean): string { - if (cbUnion.kind !== "union") throw new InternalCompilerError("emitter bug: close-override callback param not a union"); - const key = `${cbUnion.unionId}:${retServer ? "srv" : "void"}`; - let sym = emitter.closeOverrideWraps.get(key); - if (sym) return sym; - sym = mangleCloseOverrideWrap(emitter.closeOverrideWraps.size); - emitter.closeOverrideWraps.set(key, sym); - const def = emitter.unionsById.get(cbUnion.unionId); - const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; - if (undefTag < 0) throw new InternalCompilerError("emitter bug: close-override callback union lacks its undefined arm"); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_self);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_self) { /* close override wrapper */`, - ` ScrClosure *sc_inner = (ScrClosure *)scr_box_get_ref(sc_self->caps[0]); /* +1 */`, - ...(retServer - ? [ - ` ScrNetServer *sc_r = ((ScrNetServer *(*)(ScrClosure *, ScrUnion *))sc_inner->fn)(sc_inner, ${emitter.unitInstanceRef(cbUnion.unionId, undefTag)});`, - ` scr_net_server_release(sc_r); /* the chaining return is unobserved here */`, - ] - : [ - ` ((void (*)(ScrClosure *, ScrUnion *))sc_inner->fn)(sc_inner, ${emitter.unitInstanceRef(cbUnion.unionId, undefTag)});`, - ]), - ` scr_closure_release(sc_inner);`, - `}`, - ); - return sym; - } - -/** Interned per-shape dgram message adapter for the TWO-parameter - * (msg: Buffer, rinfo) listener: the rinfo RECORD is program data, so - * the adapter builds it from the runtime's parts (address/family - * borrowed and retained in; port/size by value) and calls the listener, - * which owns both params per the universal convention (msg retains like - * the net data thunk). Zero/one-param listeners take the runtime's own - * scr_dgram_msg_thunk0/1. */ - export function dgramMsgThunkFor(emitter: CEmitter, param: IrType): string { - if (param.kind !== "record") throw new InternalCompilerError("emitter bug: message listener rinfo not a record"); - const key = param.shapeId; - let sym = emitter.dgramMsgThunks.get(key); - if (!sym) { - sym = mangleDgramMsgThunk(emitter.dgramMsgThunks.size); - emitter.dgramMsgThunks.set(key, sym); - const struct = mangleRecordStruct(param.shapeId); - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_msg, ScrStr *sc_addr, ScrStr *sc_family, double sc_port, double sc_size);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_msg, ScrStr *sc_addr, ScrStr *sc_family, double sc_port, double sc_size) {`, - ` ${struct} *sc_ri = ${mangleRecordNew(param.shapeId)}();`, - ` sc_ri->${mangleField("address")} = scr_str_retain(sc_addr);`, - ` sc_ri->${mangleField("family")} = scr_str_retain(sc_family);`, - ` sc_ri->${mangleField("port")} = sc_port;`, - ` sc_ri->${mangleField("size")} = sc_size;`, - ` ((void (*)(ScrClosure *, ScrBytes *, ${struct} *))sc_cb->fn)(sc_cb, scr_bytes_retain(sc_msg), sc_ri);`, - `}`, - ); - } - return sym; - } - -/** Interned dns.lookup callback adapter, per (union id, param count): - * the runtime invokes adapter(cb, errmsg-or-NULL, address, family) with - * everything borrowed; the adapter builds the `Error | null` first - * argument (tags are program data — the frontend pinned the union) and - * calls the listener with as many arguments as it declared, each owned - * per the universal convention. */ - export function dnsLookupThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: dns.lookup callback not a func"); - const nparams = cbT.params.length; - if (nparams === 0) return "scr_dns_thunk0"; - const errT = cbT.params[0]!; - if (errT.kind !== "union") throw new InternalCompilerError("emitter bug: dns.lookup err param not a union"); - const key = `${errT.unionId}/${nparams}`; - let sym = emitter.dnsLookupThunks.get(key); - if (!sym) { - sym = mangleDnsLookupThunk(emitter.dnsLookupThunks.size); - emitter.dnsLookupThunks.set(key, sym); - const def = emitter.unionsById.get(errT.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (errTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: dns.lookup err union lacks its arms"); - } - const callSig = ["ScrClosure *", "ScrUnion *", ...(nparams >= 2 ? ["ScrStr *"] : []), ...(nparams >= 3 ? ["double"] : [])].join(", "); - const callArgs = ["sc_cb", "sc_u", ...(nparams >= 2 ? ["scr_str_retain(sc_addr)"] : []), ...(nparams >= 3 ? ["sc_family"] : [])].join(", "); - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, ScrStr *sc_err, ScrStr *sc_addr, double sc_family);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrStr *sc_err, ScrStr *sc_addr, double sc_family) {`, - ` (void)sc_addr; (void)sc_family;`, - ` ScrUnion *sc_u = sc_err`, - ` ? scr_union_new_ref(${errTag}, scr_error_new(0, sc_err), &scr_error_retain_v, &scr_error_release_v, NULL)`, - ` : ${emitter.unitInstanceRef(errT.unionId, nullTag)};`, - ` ((void (*)(${callSig}))sc_cb->fn)(${callArgs});`, - `}`, - ); - } - return sym; - } - -/** Interned fs.rename callback adapter. The runtime supplies a borrowed - * ScrError (or NULL); the adapter builds the callback's program-specific - * Error | null union, or a dyn error/null for checkJs callbacks. */ - export function fsRenameThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: fs.rename callback not a func"); - if (cbT.params.length === 0) return "scr_fs_rename_thunk0"; - const param = cbT.params[0]!; - const key = typeKey(cbT); - let sym = emitter.fsRenameThunks.get(key); - if (sym) return sym; - sym = mangleFsRenameThunk(emitter.fsRenameThunks.size); - emitter.fsRenameThunks.set(key, sym); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err);`); - if (param.kind === "dyn") { - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err) {`, - ` ScrDyn *sc_arg = sc_err ? scr_dyn_from_error(sc_err) : scr_dyn_new_null();`, - ` ((void (*)(ScrClosure *, ScrDyn *))sc_cb->fn)(sc_cb, sc_arg);`, - `}`, - ); - return sym; - } - if (param.kind !== "union") throw new InternalCompilerError("emitter bug: fs.rename error param not a union"); - const def = emitter.unionsById.get(param.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object" && a.className === "%Error") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (errTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: fs.rename error union lacks its arms"); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err) {`, - ` ScrUnion *sc_u = sc_err`, - ` ? scr_union_new_ref(${errTag}, scr_error_retain(sc_err), &scr_error_retain_v, &scr_error_release_v, NULL)`, - ` : ${emitter.unitInstanceRef(param.unionId, nullTag)};`, - ` ((void (*)(ScrClosure *, ScrUnion *))sc_cb->fn)(sc_cb, sc_u);`, - `}`, - ); - return sym; - } - -/** Callback adapter for randomBytes/pbkdf2. The runtime transfers one - * owned Buffer; the adapter materializes the callback's Error | null (or - * checked-dynamic null) first argument and either transfers or boxes the - * Buffer according to the program-specific callback signature. */ - export function cryptoBytesThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func" || cbT.params.length > 2) { - throw new InternalCompilerError("emitter bug: crypto bytes callback shape"); - } - const key = `crypto:${typeKey(cbT)}`; - let sym = emitter.fsRenameThunks.get(key); - if (sym) return sym; - sym = `sc_cryptobytes_${emitter.fsRenameThunks.size}`; - emitter.fsRenameThunks.set(key, sym); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_value);`); - const lines = [`static void ${sym}(ScrClosure *sc_cb, ScrBytes *sc_value) {`]; - const callTypes = ["ScrClosure *"]; - const callArgs = ["sc_cb"]; - const error = cbT.params[0]; - if (error !== undefined) { - if (error.kind === "dyn") { - lines.push(` ScrDyn *sc_error = scr_dyn_new_null();`); - callTypes.push("ScrDyn *"); - } else if (error.kind === "union") { - const def = emitter.unionsById.get(error.unionId); - const nullTag = def ? def.arms.findIndex((arm) => arm.kind === "nullT") : -1; - if (nullTag < 0) throw new InternalCompilerError("emitter bug: crypto callback error union lacks null"); - lines.push(` ScrUnion *sc_error = ${emitter.unitInstanceRef(error.unionId, nullTag)};`); - callTypes.push("ScrUnion *"); - } else { - throw new InternalCompilerError("emitter bug: crypto callback error param"); - } - callArgs.push("sc_error"); - } - const value = cbT.params[1]; - if (value === undefined) { - lines.push(` scr_bytes_release(sc_value);`); - } else if (value.kind === "dyn") { - lines.push(` ScrDyn *sc_result = scr_dyn_new_bytes(sc_value);`, ` scr_bytes_release(sc_value);`); - callTypes.push("ScrDyn *"); - callArgs.push("sc_result"); - } else if (value.kind === "bytes" && value.elem === "u8") { - callTypes.push("ScrBytes *"); - callArgs.push("sc_value"); - } else { - throw new InternalCompilerError("emitter bug: crypto callback value param"); - } - lines.push( - ` ((void (*)(${callTypes.join(", ")}))sc_cb->fn)(${callArgs.join(", ")});`, - `}`, - ); - emitter.walkerDefs.push(...lines); - return sym; - } - -/** Error-first Buffer callback adapter for worker-backed zlib operations. - * The runtime transfers the success Buffer and borrows the failure Error; - * exactly one is non-NULL. */ - export function zlibBytesThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func" || cbT.params.length > 2) { - throw new InternalCompilerError("emitter bug: zlib bytes callback shape"); - } - const key = `zlib:${typeKey(cbT)}`; - let sym = emitter.fsRenameThunks.get(key); - if (sym) return sym; - sym = `sc_zlibbytes_${emitter.fsRenameThunks.size}`; - emitter.fsRenameThunks.set(key, sym); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err, ScrBytes *sc_value);`); - const lines = [`static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err, ScrBytes *sc_value) {`]; - const callTypes = ["ScrClosure *"]; - const callArgs = ["sc_cb"]; - const error = cbT.params[0]; - if (error !== undefined) { - if (error.kind === "dyn") { - lines.push(` ScrDyn *sc_error = sc_err ? scr_dyn_from_error(sc_err) : scr_dyn_new_null();`); - callTypes.push("ScrDyn *"); - } else if (error.kind === "union") { - const def = emitter.unionsById.get(error.unionId); - const errTag = def ? def.arms.findIndex((arm) => arm.kind === "object" && arm.className === "%Error") : -1; - const nullTag = def ? def.arms.findIndex((arm) => arm.kind === "nullT") : -1; - if (errTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: zlib callback error union lacks arms"); - lines.push( - ` ScrUnion *sc_error = sc_err`, - ` ? scr_union_new_ref(${errTag}, scr_error_retain(sc_err), &scr_error_retain_v, &scr_error_release_v, NULL)`, - ` : ${emitter.unitInstanceRef(error.unionId, nullTag)};`, - ); - callTypes.push("ScrUnion *"); - } else { - throw new InternalCompilerError("emitter bug: zlib callback error param"); - } - callArgs.push("sc_error"); - } - const value = cbT.params[1]; - if (value === undefined) { - lines.push(` scr_bytes_release(sc_value);`); - } else if (value.kind === "dyn") { - lines.push( - ` ScrDyn *sc_result = sc_value ? scr_dyn_new_bytes(sc_value) : scr_dyn_undefined();`, - ` scr_bytes_release(sc_value);`, - ); - callTypes.push("ScrDyn *"); - callArgs.push("sc_result"); - } else if (value.kind === "bytes" && value.elem === "u8") { - callTypes.push("ScrBytes *"); - callArgs.push("sc_value"); - } else { - throw new InternalCompilerError("emitter bug: zlib callback value param"); - } - lines.push( - ` ((void (*)(${callTypes.join(", ")}))sc_cb->fn)(${callArgs.join(", ")});`, - `}`, - ); - emitter.walkerDefs.push(...lines); - return sym; - } - -/** Interned CONNECT-listener adapter for a UNION socket slot — the h2 - * compat listener (`(req, resOrSocket: Http2ServerResponse | net.Socket) - * => void`): the runtime fires (cb, req, sock, head) like an upgrade; - * the adapter wraps the socket at the union's netSocket arm (its tag is - * program data) and drops `head` when the listener declares two params. - * All three runtime args arrive +1; the union takes the socket's. */ - export function connectSockThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func" || cbT.params[1]?.kind !== "union") { - throw new InternalCompilerError("emitter bug: connect listener union shape (frontend must fence)"); - } - const key = typeKey(cbT); - let sym = emitter.connectSockThunks.get(key); - if (sym) return sym; - sym = mangleConnectSockThunk(emitter.connectSockThunks.size); - emitter.connectSockThunks.set(key, sym); - const sockT = cbT.params[1]; - const def = emitter.unionsById.get(sockT.unionId); - const tag = def ? def.arms.findIndex((a) => a.kind === "netSocket") : -1; - if (tag < 0) throw new InternalCompilerError("emitter bug: connect listener union lacks its socket arm"); - const three = cbT.params.length === 3; - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, ScrHttpReq *sc_req, ScrNetSocket *sc_sock, ScrBytes *sc_head);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrHttpReq *sc_req, ScrNetSocket *sc_sock, ScrBytes *sc_head) {`, - ...(three ? [] : [` scr_bytes_release(sc_head);`]), - ` ScrUnion *sc_u = scr_union_new_ref(${tag}, sc_sock, &scr_net_sock_retain_v, &scr_net_sock_release_v, NULL);`, - three - ? ` ((void (*)(ScrClosure *, ScrHttpReq *, ScrUnion *, ScrBytes *))sc_cb->fn)(sc_cb, sc_req, sc_u, sc_head);` - : ` ((void (*)(ScrClosure *, ScrHttpReq *, ScrUnion *))sc_cb->fn)(sc_cb, sc_req, sc_u);`, - `}`, - ); - return sym; - } - - export function connectResThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: connect listener shape"); - const key = `res:${typeKey(cbT)}`; - let sym = emitter.connectSockThunks.get(key); - if (sym) return sym; - sym = mangleConnectResThunk(emitter.connectSockThunks.size); - emitter.connectSockThunks.set(key, sym); - const p1 = cbT.params[1]; - const def = p1?.kind === "union" ? emitter.unionsById.get(p1.unionId) : undefined; - const tag = def ? def.arms.findIndex((a) => a.kind === "httpRes") : -1; - if (p1?.kind === "union" && tag < 0) throw new InternalCompilerError("emitter bug: connect listener union lacks its response arm"); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrHttpReq *sc_req, ScrHttpRes *sc_res);`); - const second = p1?.kind === "union" - ? `ScrUnion *sc_u = scr_union_new_ref(${tag}, sc_res, &scr_http_res_retain_v, &scr_http_res_release_v, NULL);` - : ""; - const arg = p1?.kind === "union" ? "sc_u" : "sc_res"; - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrHttpReq *sc_req, ScrHttpRes *sc_res) {`, - ...(second ? [` ${second}`] : []), - ...(cbT.params.length === 0 - ? [" scr_http_req_release(sc_req);", " scr_http_res_release(sc_res);", " ((void (*)(ScrClosure *))sc_cb->fn)(sc_cb);"] - : cbT.params.length === 1 - ? [" scr_http_res_release(sc_res);", " ((void (*)(ScrClosure *, ScrHttpReq *))sc_cb->fn)(sc_cb, sc_req);"] - : [` ((void (*)(ScrClosure *, ScrHttpReq *, ${p1?.kind === "union" ? "ScrUnion" : "ScrHttpRes"} *))sc_cb->fn)(sc_cb, sc_req, ${arg});`]), - `}`, - ); - return sym; - } - -/** Interned net.connect-lookup answer thunk: the fn of the runtime-minted - * callback the caller's `lookup` option receives (the SNI-answer - * pattern). COMPILED code calls it per its static type — `(err: - * ErrnoException | null, addresses: { address: string, ... }[]) => - * void` — so the thunk decodes the program-interned err union (null - * tag) and the addresses record's field layout down to the runtime's - * (has_err, message, ip-list) answer. Params arrive +1 and release - * here; the extracted message/ips hand over +1. */ - export function netLookupAnswerThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func" || cbT.params.length !== 2) { - throw new InternalCompilerError("emitter bug: lookup answer cb shape (frontend must fence)"); - } - const key = typeKey(cbT); - let sym = emitter.netLookupAnswerThunks.get(key); - if (sym) return sym; - sym = mangleNetLookupAnswerThunk(emitter.netLookupAnswerThunks.size); - emitter.netLookupAnswerThunks.set(key, sym); - const errT = cbT.params[0]!; - if (errT.kind !== "union") throw new InternalCompilerError("emitter bug: lookup answer err param not a union"); - const errDef = emitter.unionsById.get(errT.unionId); - const nullTag = errDef ? errDef.arms.findIndex((a) => a.kind === "nullT") : -1; - if (nullTag < 0) throw new InternalCompilerError("emitter bug: lookup answer err union lacks its null arm"); - const addrsT = cbT.params[1]!; - if (addrsT.kind !== "array" || addrsT.elem.kind !== "record") { - throw new InternalCompilerError("emitter bug: lookup answer addresses param not a record array"); - } - const recStruct = mangleRecordStruct(addrsT.elem.shapeId); - const recRelease = mangleRecordRelease(addrsT.elem.shapeId); - const addrField = mangleField("address"); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_self, ScrUnion *sc_err, ScrArr *sc_addrs);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_self, ScrUnion *sc_err, ScrArr *sc_addrs) {`, - ` bool sc_has_err = sc_err->tag != ${nullTag};`, - ` ScrStr *sc_msg = NULL;`, - ` if (sc_has_err) sc_msg = scr_str_retain(((ScrError *)scr_union_peek(sc_err))->message);`, - ` ScrArr *sc_ips = NULL;`, - ` if (!sc_has_err) {`, - ` double sc_n = scr_arr_len(sc_addrs);`, - ` sc_ips = scr_arr_new(SCR_ELEM_STR, (size_t)sc_n);`, - ` for (double sc_i = 0; sc_i < sc_n; sc_i++) {`, - ` ${recStruct} *sc_r = (${recStruct} *)scr_arr_get_ref(sc_addrs, sc_i);`, - ` scr_arr_push_ref(sc_ips, scr_str_retain(sc_r->${addrField}));`, - ` ${recRelease}(sc_r);`, - ` }`, - ` }`, - ` scr_net_lookup_answer(sc_self, sc_has_err, sc_msg, sc_ips);`, - ` scr_str_release(sc_msg);`, - ` scr_arr_release(sc_ips);`, - ` scr_union_release(sc_err);`, - ` scr_arr_release(sc_addrs);`, - `}`, - ); - return sym; - } - -/** Interned SNI answer-closure thunk: the fn of the runtime-minted `cb` - * a TLS server's SNICallback receives. COMPILED code calls cb per its - * static type — `(err: Error | null, ctx?: SecureContext) => void`, or - * a shorter declared shape — so the thunk's signature is that ABI, and - * it decodes the program-interned unions (tags are program data) down - * to the runtime's (has_err, ctx-or-NULL) answer. The thunk owns its - * union params per the universal convention (the ctx payload retains - * +1 before its union releases; ownership moves to the runtime). */ - export function sniAnswerThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: SNI answer cb not a func"); - const key = typeKey(cbT); - let sym = emitter.sniAnswerThunks.get(key); - if (sym) return sym; - sym = mangleSniAnswerThunk(emitter.sniAnswerThunks.size); - emitter.sniAnswerThunks.set(key, sym); - const nparams = cbT.params.length; - const params = ["ScrClosure *sc_self"]; - const body: string[] = []; - let hasErr = "false"; - if (nparams >= 1) { - const errT = cbT.params[0]!; - if (errT.kind !== "union") throw new InternalCompilerError("emitter bug: SNI answer err param not a union"); - const def = emitter.unionsById.get(errT.unionId); - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (nullTag < 0) throw new InternalCompilerError("emitter bug: SNI answer err union lacks its null arm"); - params.push("ScrUnion *sc_err"); - hasErr = `sc_err->tag != ${nullTag}`; - } - let ctx = "NULL"; - if (nparams >= 2) { - const ctxT = cbT.params[1]!; - if (ctxT.kind !== "union") throw new InternalCompilerError("emitter bug: SNI answer ctx param not a union"); - const def = emitter.unionsById.get(ctxT.unionId); - const ctxTag = def ? def.arms.findIndex((a) => a.kind === "secureCtx") : -1; - if (ctxTag < 0) throw new InternalCompilerError("emitter bug: SNI answer ctx union lacks its SecureContext arm"); - params.push("ScrUnion *sc_ctx"); - body.push( - ` ScrSecureCtx *sc_c = sc_ctx->tag == ${ctxTag} ? scr_secure_ctx_retain((ScrSecureCtx *)scr_union_peek(sc_ctx)) : NULL;`, - ` scr_union_release(sc_ctx);`, - ); - ctx = "sc_c"; - } - body.push(` bool sc_has_err = ${hasErr};`); - if (nparams >= 1) body.push(` scr_union_release(sc_err);`); - body.push(` scr_tls_sni_answer(sc_self, sc_has_err, ${ctx});`); - emitter.walkerProtos.push(`static void ${sym}(${params.join(", ")});`); - emitter.walkerDefs.push(`static void ${sym}(${params.join(", ")}) {`, ...body, `}`); - return sym; - } - -/** Interned Promise.race fulfillment adapter: converts a settled entry's - * payload (inner type `from`) into the result promise's inner type `to` - * and fulfills the destination. Three shapes, mirroring the frontend's - * compatibility fence: identical types share the runtime's raw copy; - * a plain entry under a union result wraps into its arm; a sub-union - * entry re-tags arm-wise (canonical arm order makes the mapping a - * typeKey lookup). Rejections never reach adapters — the runtime copies - * them raw. */ - export function raceAdapterFor(emitter: CEmitter, from: IrType, to: IrType): string { - if (typeEquals(from, to)) return "scr_promise_adapt_copy"; - const key = `${typeKey(from)}=>${typeKey(to)}`; - let sym = emitter.raceThunks.get(key); - if (sym) return sym; - sym = mangleRaceThunk(emitter.raceThunks.size); - emitter.raceThunks.set(key, sym); - if (to.kind !== "union") throw new InternalCompilerError("emitter bug: race adapter to a non-union"); - const toDef = emitter.unionsById.get(to.unionId); - if (!toDef) throw new InternalCompilerError("emitter bug: race adapter to an unknown union"); - const tagOf = (t: IrType): number => { - const tag = toDef.arms.findIndex((a) => typeEquals(a, t)); - if (tag < 0) throw new InternalCompilerError("emitter bug: race adapter arm missing (frontend must fence)"); - return tag; - }; - const rv = vAdapters(to); - const fulfill = (value: string): string => - `scr_promise_fulfill_ref(sc_dst, ${value}, ${rv.retain}, ${rv.release}, ${emitter.traceArgC(to)});`; - emitter.walkerProtos.push(`static void ${sym}(ScrPromise *sc_dst, ScrPromise *sc_src);`); - if (from.kind !== "union") { - // One arm wrap, straight off the payload accessors. - const tag = tagOf(from); - let value: string; - switch (from.kind) { - case "f64": - value = `scr_union_new_f64(${tag}, scr_promise_payload_f64(sc_src))`; - break; - case "bool": - value = `scr_union_new_bool(${tag}, scr_promise_payload_bool(sc_src))`; - break; - case "string": - value = `scr_union_new_ref(${tag}, scr_promise_payload_str(sc_src), scr_str_retain_v, scr_str_release_v, NULL)`; - break; - default: { - const fv = vAdapters(from); - value = `scr_union_new_ref(${tag}, scr_promise_payload_ref(sc_src), ${fv.retain}, ${fv.release}, ${emitter.traceArgC(from)})`; - } - } - emitter.walkerDefs.push( - `static void ${sym}(ScrPromise *sc_dst, ScrPromise *sc_src) {`, - ` ${fulfill(value)}`, - `}`, - ); - return sym; - } - // Sub-union re-tag: switch over the entry's arms, rebuild under the - // result's tags (payloads retained through each arm's own adapters). - const fromDef = emitter.unionsById.get(from.unionId); - if (!fromDef) throw new InternalCompilerError("emitter bug: race adapter from an unknown union"); - const cases = fromDef.arms.map((arm, i) => { - const tag = tagOf(arm); - let build: string; - if (isUnitType(arm)) { - build = emitter.unitInstanceRef(to.unionId, tag); - } else if (arm.kind === "f64") { - build = `scr_union_new_f64(${tag}, scr_union_get_f64(sc_u))`; - } else if (arm.kind === "bool") { - build = `scr_union_new_bool(${tag}, scr_union_get_bool(sc_u))`; - } else { - const av = vAdapters(arm); - build = `scr_union_new_ref(${tag}, ${av.retain}(scr_union_peek(sc_u)), ${av.retain}, ${av.release}, ${emitter.traceArgC(arm)})`; - } - return ` case ${i}: sc_v = ${build}; break;`; - }); - emitter.walkerDefs.push( - `static void ${sym}(ScrPromise *sc_dst, ScrPromise *sc_src) {`, - ` ScrUnion *sc_u = (ScrUnion *)scr_promise_payload_ref(sc_src);`, - ` ScrUnion *sc_v = NULL;`, - ` switch (sc_u->tag) {`, - ...cases, - ` default: break;`, - ` }`, - ` scr_union_release(sc_u);`, - ` ${fulfill("sc_v")}`, - `}`, - ); - return sym; - } - -/** Interned generator-resume result builder: reads the post-resume state - * of a generator into a fresh IteratorResult record `{ done, value }`. - * One helper per generator type (every next/return/throw on it shares - * the record shape). The value slot: while suspended, the yielded value - * moves out of the OUT slot into its arm of V (the yield type retags - * arm-wise into a superset V, the raceAdapterFor technique); once done, - * a present completion value wraps the same way and an empty OUT is - * JS's undefined (the interned undefined arm — or the dyn undefined - * when V is dyn, the any/unknown channel). */ - export function genResultThunkFor( - emitter: CEmitter, - genT: IrType & { kind: "generator" }, - recT: IrType & { kind: "record" }, - ): string { - const key = typeKey(genT); - let sym = emitter.genResThunks.get(key); - if (sym) return sym; - sym = mangleGenResThunk(emitter.genResThunks.size); - emitter.genResThunks.set(key, sym); - const struct = mangleRecordStruct(recT.shapeId); - const shape = emitter.mod.records?.find((r) => r.id === recT.shapeId); - const valueT = shape?.fields.find((f) => f.name === "value")?.type; - if (!valueT) throw new InternalCompilerError("emitter bug: genResume record lacks its value field"); - const lines: string[] = [ - `static ${struct} *${sym}(ScrGen *sc_g) {`, - ` ${struct} *sc_r = ${mangleRecordNew(recT.shapeId)}();`, - ` bool sc_d = scr_gen_done(sc_g);`, - ` sc_r->${mangleField("done")} = sc_d;`, - ]; - if (valueT.kind === "dyn") { - // The any/unknown channel: OUT holds a dyn (or nothing — undefined). - lines.push( - ` sc_r->${mangleField("value")} = scr_gen_out_has(sc_g)`, - ` ? (ScrDyn *)scr_gen_take_out_ref(sc_g)`, - ` : scr_dyn_retain(scr_dyn_undefined());`, - ); - } else { - if (valueT.kind !== "union") { - throw new InternalCompilerError("emitter bug: genResume value slot is neither dyn nor a union"); - } - const def = emitter.unionsById.get(valueT.unionId); - if (!def) throw new InternalCompilerError("emitter bug: genResume value union unknown"); - const tagOf = (t: IrType): number => { - const tag = def.arms.findIndex((a) => typeEquals(a, t)); - if (tag < 0) throw new InternalCompilerError("emitter bug: genResume value union lacks an arm"); - return tag; - }; - const undefTag = def.arms.findIndex((a) => a.kind === "undefinedT"); - if (undefTag < 0) throw new InternalCompilerError("emitter bug: genResume value union lacks undefined"); - // Lines that leave the wrapped value in `sc_v`, taking OUT's payload. - const wrapFrom = (srcT: IrType): string[] => { - if (srcT.kind === "f64") { - return [` sc_v = scr_union_new_f64(${tagOf(srcT)}, scr_gen_take_out_f64(sc_g));`]; - } - if (srcT.kind === "bool") { - return [` sc_v = scr_union_new_bool(${tagOf(srcT)}, scr_gen_take_out_bool(sc_g));`]; - } - if (srcT.kind !== "union") { - const v = vAdapters(srcT); - return [ - ` sc_v = scr_union_new_ref(${tagOf(srcT)}, scr_gen_take_out_ref(sc_g), ${v.retain}, ${v.release}, ${emitter.traceArgC(srcT)});`, - ]; - } - // A union channel: OUT holds the union box itself. Identical V - // passes through; a superset V retags arm-wise (payloads retained - // through each arm's own adapters, then the source box drops). - if (typeEquals(srcT, valueT)) { - return [` sc_v = (ScrUnion *)scr_gen_take_out_ref(sc_g);`]; - } - const srcDef = emitter.unionsById.get(srcT.unionId); - if (!srcDef) throw new InternalCompilerError("emitter bug: genResume channel union unknown"); - const cases = srcDef.arms.map((arm, i) => { - const tag = tagOf(arm); - let build: string; - if (isUnitType(arm)) { - build = emitter.unitInstanceRef(valueT.unionId, tag); - } else if (arm.kind === "f64") { - build = `scr_union_new_f64(${tag}, scr_union_get_f64(sc_u))`; - } else if (arm.kind === "bool") { - build = `scr_union_new_bool(${tag}, scr_union_get_bool(sc_u))`; - } else { - const av = vAdapters(arm); - build = `scr_union_new_ref(${tag}, ${av.retain}(scr_union_peek(sc_u)), ${av.retain}, ${av.release}, ${emitter.traceArgC(arm)})`; - } - return ` case ${i}: sc_v = ${build}; break;`; - }); - return [ - ` { ScrUnion *sc_u = (ScrUnion *)scr_gen_take_out_ref(sc_g);`, - ` switch (sc_u->tag) {`, - ...cases, - ` default: sc_v = ${emitter.unitInstanceRef(valueT.unionId, undefTag)}; break;`, - ` }`, - ` scr_union_release(sc_u); }`, - ]; - }; - const undefLine = ` sc_v = ${emitter.unitInstanceRef(valueT.unionId, undefTag)};`; - lines.push(` ScrUnion *sc_v;`); - lines.push(` if (!sc_d) {`); - // yieldT VOID marks a generator that can never yield (TS's `never`): - // the suspended branch is unreachable — the undefined arm keeps the - // C total. - lines.push(...(genT.yieldT.kind === "void" ? [undefLine] : wrapFrom(genT.yieldT))); - lines.push(` } else if (scr_gen_out_has(sc_g)) {`); - lines.push(...(genT.retT.kind === "void" ? [undefLine] : wrapFrom(genT.retT))); - lines.push(` } else {`, undefLine, ` }`); - lines.push(` sc_r->${mangleField("value")} = sc_v;`); - } - lines.push(` return sc_r;`, `}`); - emitter.walkerProtos.push(`static ${struct} *${sym}(ScrGen *sc_g);`); - emitter.walkerDefs.push(...lines, ``); - return sym; - } - -/** Interned per inner-type resolve thunk for ref-kind new Promise. */ - export function resolveThunkFor(emitter: CEmitter, inner: IrType): string { - const key = typeKey(inner); - let sym = emitter.resolveThunks.get(key); - if (!sym) { - sym = mangleResolveThunk(emitter.resolveThunks.size); - emitter.resolveThunks.set(key, sym); - const v = vAdapters(inner); - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_self, ${cDecl(inner, "sc_v")});`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_self, ${cDecl(inner, "sc_v")}) {`, - ` scr_resolve_ref_impl(sc_self, sc_v, ${v.retain}, ${v.release}, ${emitter.traceArgC(inner)});`, - `}`, - ); - } - return sym; - } - -/** The EventEmitter listener invoke adapter for one listener signature: - * `void (ScrClosure *, va_list)` — va_args exactly the listener's own - * parameter prefix of the emit site's typed tail (the frontend unified - * the tuple, so reads agree with writes), retains each refcounted value - * (the callee owns +1 per the universal convention; f64/bool copy), and - * calls cb->fn behind its exact C signature. Interned per func-type key. */ -export function emitterInvokeThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") { - throw new InternalCompilerError("emitter bug: emitter.on listener not a func (frontend must fence)"); - } - const key = typeKey(cbT); - let sym = emitter.emitterInvokeThunks.get(key); - if (sym) return sym; - sym = mangleEmitterInvokeThunk(emitter.emitterInvokeThunks.size); - emitter.emitterInvokeThunks.set(key, sym); - const reads: string[] = []; - const passed: string[] = []; - cbT.params.forEach((p, i) => { - const name = `sc_a${i}`; - if (p.kind === "f64" || p.kind === "date") { - reads.push(` double ${name} = va_arg(sc_ap, double);`); - passed.push(name); - } else if (p.kind === "bool") { - // bool promotes to int through a variadic call. - reads.push(` bool ${name} = (bool)va_arg(sc_ap, int);`); - passed.push(name); - } else { - reads.push(` ${cDecl(p, name)} = va_arg(sc_ap, ${cType(p).trim()});`); - passed.push(retainCallC(p, name)); - } - }); - const sigParams = ["ScrClosure *", ...cbT.params.map((p) => cType(p).trim())].join(", "); - // The call goes through the listener's TRUE signature; a non-void - // result is discarded (refcounted ones released) — Node ignores - // listener return values. - const retC = cbT.ret.kind === "void" ? "void" : cType(cbT.ret).trim(); - const invoke = `((${retC} (*)(${sigParams}))sc_cb->fn)(${["sc_cb", ...passed].join(", ")})`; - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, va_list sc_ap);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, va_list sc_ap) {`, - ...(cbT.params.length === 0 ? [` (void)sc_ap;`] : reads), - ...(cbT.ret.kind === "void" - ? [` ${invoke};`] - : isRefCounted(cbT.ret) - ? [` ${retC} sc_r = ${invoke};`, ` if (sc_r) ${releaseCallC(cbT.ret, "sc_r")};`] - : [` (void)${invoke};`]), - `}`, - ``, - ); - return sym; -} - -/** child_process.execFile's error-first callback adapter. The runtime owns - * one +1 callback plus +1 error/output values at settlement; the adapter - * wraps the nullable error with this program's union tags, transfers every - * declared argument into the compiled closure, and releases ignored suffix - * values for callbacks that declare fewer than three parameters. */ -export function execFileThunkFor(emitter: CEmitter, cbT: IrType & { kind: "func" }): string { - const key = `exec:${typeKey(cbT)}`; - let sym = emitter.childExitThunks.get(key); - if (sym) return sym; - sym = mangleChildExitThunk(emitter.childExitThunks.size); - emitter.childExitThunks.set(key, sym); - const errorParam = cbT.params[0]; - let errorExpr: string | null = null; - if (errorParam !== undefined) { - if (errorParam.kind !== "union") throw new InternalCompilerError("emitter bug: execFile error param not a union"); - const def = emitter.unionsById.get(errorParam.unionId); - const errorTag = def ? def.arms.findIndex((arm) => arm.kind === "object" && arm.className === "%Error") : -1; - const nullTag = def ? def.arms.findIndex((arm) => arm.kind === "nullT") : -1; - const errorArm = errorTag >= 0 ? def?.arms[errorTag] : undefined; - if (nullTag < 0 || !errorArm) throw new InternalCompilerError("emitter bug: execFile error union lacks its arms"); - errorExpr = `sc_err ? scr_union_new_ref(${errorTag}, sc_err, &scr_error_retain_v, &scr_error_release_v, ${emitter.traceArgC(errorArm)}) : ${emitter.unitInstanceRef(errorParam.unionId, nullTag)}`; - } - const callbackArgs = [ - "sc_cb", - ...(errorExpr === null ? [] : ["sc_e"]), - ...(cbT.params.length >= 2 ? ["sc_out"] : []), - ...(cbT.params.length >= 3 ? ["sc_stderr"] : []), - ]; - const callbackTypes = [ - "ScrClosure *", - ...(errorExpr === null ? [] : ["ScrUnion *"]), - ...(cbT.params.length >= 2 ? ["ScrStr *"] : []), - ...(cbT.params.length >= 3 ? ["ScrStr *"] : []), - ]; - emitter.walkerProtos.push( - `static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err, ScrStr *sc_out, ScrStr *sc_stderr);`, - ); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrError *sc_err, ScrStr *sc_out, ScrStr *sc_stderr) {`, - ...(errorExpr === null ? [` scr_error_release(sc_err);`] : [` ScrUnion *sc_e = ${errorExpr};`]), - ...(cbT.params.length < 2 ? [` scr_str_release(sc_out);`] : []), - ...(cbT.params.length < 3 ? [` scr_str_release(sc_stderr);`] : []), - ` ((void (*)(${callbackTypes.join(", ")}))sc_cb->fn)(${callbackArgs.join(", ")});`, - `}`, - ); - return sym; -} - -/** Fork IPC message adapter: parse results arrive as a borrowed dyn tree, - * then validate into the listener's static JSON shape. Async listener - * promises are deliberately released without attaching a handler, matching - * EventEmitter's ignored return value. */ -export function ipcMessageThunkFor(emitter: CEmitter, cbT: IrType & { kind: "func" }): string { - const key = `ipcmsg:${typeKey(cbT)}`; - let sym = emitter.childExitThunks.get(key); - if (sym) return sym; - sym = mangleChildExitThunk(emitter.childExitThunks.size); - emitter.childExitThunks.set(key, sym); - const param = cbT.params[0]; - const body: string[] = []; - const callbackTypes = ["ScrClosure *"]; - const callbackArgs = ["sc_cb"]; - if (param !== undefined) { - const value = param.kind === "dyn" - ? "scr_dyn_retain(sc_message)" - : `${emitter.dynCheckHelper(param)}(sc_message, NULL)`; - body.push(` ${cDecl(param, "sc_value")} = ${value};`); - if (param.kind !== "dyn") { - body.push( - ` if (scr_exc_pending()) {`, - ...(isRefCounted(param) ? [` ${releaseCallC(param, "sc_value")};`] : []), - ` return;`, - ` }`, - ); - } - callbackTypes.push(cType(param).trim()); - callbackArgs.push("sc_value"); - } else { - body.push(` (void)sc_message;`); - } - const invoke = `((` + cType(cbT.ret).trim() + ` (*)(${callbackTypes.join(", ")}))sc_cb->fn)(${callbackArgs.join(", ")})`; - if (cbT.ret.kind === "void") body.push(` ${invoke};`); - else body.push(` ${cDecl(cbT.ret, "sc_result")} = ${invoke};`, ` ${releaseCallC(cbT.ret, "sc_result")};`); - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrDyn *sc_message);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrDyn *sc_message) {`, - ...body, - `}`, - ); - return sym; -} - -/** Fork IPC send completion adapter. The nullable +1 runtime Error moves - * into the callback's Error|null union, or is released for a zero-argument - * callback. */ -export function ipcSendThunkFor(emitter: CEmitter, cbT: IrType & { kind: "func" }): string { - const key = `ipcsend:${typeKey(cbT)}`; - let sym = emitter.childExitThunks.get(key); - if (sym) return sym; - sym = mangleChildExitThunk(emitter.childExitThunks.size); - emitter.childExitThunks.set(key, sym); - const param = cbT.params[0]; - const body: string[] = []; - if (param === undefined) { - body.push( - ` scr_error_release(sc_error);`, - ` ((void (*)(ScrClosure *))sc_cb->fn)(sc_cb);`, - ); - } else { - if (param.kind !== "union") throw new InternalCompilerError("emitter bug: IPC send callback param not a union"); - const def = emitter.unionsById.get(param.unionId); - const errorTag = def ? def.arms.findIndex((arm) => arm.kind === "object" && arm.className === "%Error") : -1; - const nullTag = def ? def.arms.findIndex((arm) => arm.kind === "nullT") : -1; - const errorArm = errorTag >= 0 ? def?.arms[errorTag] : undefined; - if (errorTag < 0 || nullTag < 0 || !errorArm) { - throw new InternalCompilerError("emitter bug: IPC send callback union lacks Error|null"); - } - body.push( - ` ScrUnion *sc_value = sc_error`, - ` ? scr_union_new_ref(${errorTag}, sc_error, &scr_error_retain_v, &scr_error_release_v, ${emitter.traceArgC(errorArm)})`, - ` : ${emitter.unitInstanceRef(param.unionId, nullTag)};`, - ` ((void (*)(ScrClosure *, ScrUnion *))sc_cb->fn)(sc_cb, sc_value);`, - ); - } - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ScrError *sc_error);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ScrError *sc_error) {`, - ...body, - `}`, - ); - return sym; -} - -/** The stream completion-callback closure fn for one done func type: the - * `callback` a user's write/final/destroy/transform/flush receives. The - * closure's one capture box holds the stream (+1); calling it unwraps the - * (optional) error/data union arguments and reports completion to the - * runtime. Kind names the runtime entry: "w" scr_stream_write_done, "f" - * scr_stream_final_done, "d" scr_stream_destroy_done, "t" - * scr_stream_transform_done, "l" scr_stream_flush_done. Args arrive - * callee-owned (+1) per the universal convention and are released here. */ -function streamDoneFnFor(emitter: CEmitter, kind: "w" | "f" | "d" | "t" | "l", doneT: IrType): string { - if (doneT.kind !== "func") { - throw new InternalCompilerError("emitter bug: stream done callback not a func (frontend must fence)"); - } - const key = `${kind}:${typeKey(doneT)}`; - let sym = emitter.streamDoneFns.get(key); - if (sym) return sym; - sym = mangleStreamDoneFn(emitter.streamDoneFns.size); - emitter.streamDoneFns.set(key, sym); - const params: string[] = ["ScrClosure *sc_self"]; - const body: string[] = [ - ` ScrStream *sc_s = (ScrStream *)scr_box_get_ref(sc_self->caps[0]); /* +1 */`, - ` ScrError *sc_err = NULL;`, - ]; - const errT = doneT.params[0]; - if (errT !== undefined) { - if (errT.kind !== "union") throw new InternalCompilerError("emitter bug: stream done err param not a union"); - const def = emitter.unionsById.get(errT.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1; - if (errTag < 0) throw new InternalCompilerError("emitter bug: stream done err union lacks its Error arm"); - params.push("ScrUnion *sc_e"); - body.push( - ` if (sc_e && sc_e->tag == ${errTag}) sc_err = scr_error_retain((ScrError *)scr_union_peek(sc_e));`, - ); - } - const dataLines: string[] = [` ScrBytes *sc_data = NULL;`, ` ScrStr *sc_dataStr = NULL;`]; - const dataT = doneT.params[1]; - if (kind === "t" || kind === "l") { - body.push(...dataLines); - if (dataT !== undefined) { - if (dataT.kind !== "union") throw new InternalCompilerError("emitter bug: stream done data param not a union"); - const def = emitter.unionsById.get(dataT.unionId); - const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1; - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - params.push("ScrUnion *sc_d"); - body.push( - ...(bytesTag >= 0 - ? [` if (sc_d && sc_d->tag == ${bytesTag}) sc_data = scr_bytes_retain((ScrBytes *)scr_union_peek(sc_d));`] - : []), - ...(strTag >= 0 - ? [` if (sc_d && sc_d->tag == ${strTag}) sc_dataStr = scr_str_retain((ScrStr *)scr_union_peek(sc_d));`] - : []), - ); - } - } - const entry = - kind === "w" ? "scr_stream_write_done" : - kind === "f" ? "scr_stream_final_done" : - kind === "d" ? "scr_stream_destroy_done" : - kind === "t" ? "scr_stream_transform_done" : "scr_stream_flush_done"; - const tail = - kind === "t" || kind === "l" - ? `${entry}(sc_s, sc_err, sc_data, sc_dataStr);` - : `${entry}(sc_s, sc_err);`; - emitter.walkerProtos.push(`static void ${sym}(${params.join(", ")});`); - emitter.walkerDefs.push( - `static void ${sym}(${params.join(", ")}) {`, - ...body, - ` ${tail} /* moves sc_err/sc_data; borrows sc_s */`, - ` scr_stream_release(sc_s);`, - ...(errT !== undefined ? [` if (sc_e) scr_union_release(sc_e);`] : []), - ...((kind === "t" || kind === "l") && dataT !== undefined ? [` if (sc_d) scr_union_release(sc_d);`] : []), - `}`, - ``, - ); - return sym; -} - -/** The stream-'data' listener invoke adapter for one listener func type: - * the runtime's 'data' emission carries BOTH payload slots (ScrBytes *b, - * ScrStr *s — exactly one non-NULL; encoded streams deliver strings), - * and this thunk unwraps the listener's declared side. A listener - * declaring the WRONG side for the stream's runtime mode gets a clear - * TypeError instead of a type-confused payload (Node, untyped, would - * just hand the value over — SEMANTICS.md). Zero-parameter listeners - * ignore both slots; DYN-parameter listeners (the JS lane's adapter, - * emitter.onDataDyn) box by tag, which is always right. */ -export function streamDataThunkFor(emitter: CEmitter, cbT: IrType): string { - if (cbT.kind !== "func") { - throw new InternalCompilerError("emitter bug: stream data listener not a func (frontend must fence)"); - } - const key = `data:${typeKey(cbT)}`; - let sym = emitter.emitterInvokeThunks.get(key); - if (sym) return sym; - sym = mangleEmitterInvokeThunk(emitter.emitterInvokeThunks.size); - emitter.emitterInvokeThunks.set(key, sym); - const p = cbT.params[0]; - if (cbT.params.length > 1 || (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn")) { - throw new InternalCompilerError("emitter bug: stream data listener param shape (frontend must fence)"); - } - const body: string[] = [ - ` ScrBytes *sc_b = va_arg(sc_ap, ScrBytes *);`, - ` ScrStr *sc_s = va_arg(sc_ap, ScrStr *);`, - ]; - const passed: string[] = ["sc_cb"]; - if (p === undefined) { - body.push(` (void)sc_b;`, ` (void)sc_s;`); - } else if (p.kind === "bytes") { - body.push( - ` if (!sc_b) {`, - ` static const char sc_m[] = "a 'data' listener declaring a Buffer chunk received a string (the stream has an encoding set)";`, - ` scr_throw_error_msg(SCR_ERR_TYPE, sc_m, sizeof sc_m - 1);`, - ` return;`, - ` }`, - ); - passed.push("scr_bytes_retain(sc_b)"); - } else if (p.kind === "string") { - body.push( - ` if (!sc_s) {`, - ` static const char sc_m[] = "a 'data' listener declaring a string chunk received a Buffer (call setEncoding, or declare the chunk as a Buffer)";`, - ` scr_throw_error_msg(SCR_ERR_TYPE, sc_m, sizeof sc_m - 1);`, - ` return;`, - ` }`, - ); - passed.push("scr_str_retain(sc_s)"); - } else { - // dyn: box by runtime tag — the JS lane's adapter parameter. - body.push(` ScrDyn *sc_d = sc_b ? scr_dyn_new_buffer(sc_b) : scr_dyn_new_str(sc_s);`); - passed.push("sc_d"); - } - const sigParams = ["ScrClosure *", ...cbT.params.map((q) => cType(q).trim())].join(", "); - const retC = cbT.ret.kind === "void" ? "void" : cType(cbT.ret).trim(); - const invoke = `((${retC} (*)(${sigParams}))sc_cb->fn)(${passed.join(", ")})`; - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, va_list sc_ap);`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, va_list sc_ap) {`, - ...body, - ...(cbT.ret.kind === "void" - ? [` ${invoke};`] - : isRefCounted(cbT.ret) - ? [` ${retC} sc_r = ${invoke};`, ` if (sc_r) ${releaseCallC(cbT.ret, "sc_r")};`] - : [` (void)${invoke};`]), - `}`, - ``, - ); - return sym; -} - -/** The stream option-callback invoke adapter for one (kind, callback - * type): the runtime calls the user's read/write/final/destroy/transform/ - * flush closure through it. The stream rides first (the leading `this` - * param every option callback's lifted fn takes); the user may have - * declared any PREFIX of the Node signature, so the adapter passes - * exactly the declared prefix (retaining each ref per the callee-owns - * convention) and materializes the completion-callback closure only when - * declared — an undeclared completion callback means the operation can - * never complete, exactly Node. Kinds: "r" read(size), "w" write(chunk, - * enc, cb), "f" final(cb), "d" destroy(err, cb), "t" transform(chunk, - * enc, cb), "l" flush(cb). */ -export function streamCbThunkFor(emitter: CEmitter, kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { - if (cbT.kind !== "func") { - throw new InternalCompilerError("emitter bug: stream option callback not a func (frontend must fence)"); - } - const key = `${kind}:${typeKey(cbT)}`; - let sym = emitter.streamCbThunks.get(key); - if (sym) return sym; - sym = mangleStreamCbThunk(emitter.streamCbThunks.size); - emitter.streamCbThunks.set(key, sym); - // The runtime-facing C signatures, per kind. - const runtimeParams = - kind === "r" ? ["ScrStream *sc_s", "double sc_size"] : - kind === "w" || kind === "t" ? ["ScrStream *sc_s", "ScrBytes *sc_chunk"] : - kind === "d" || kind === "e" ? ["ScrStream *sc_s", "ScrError *sc_err"] : - ["ScrStream *sc_s"]; - // The user callback's declared prefix: params[0] is the stream (`this`) - // for the inline forms; a THISLESS callback (the checked-dynamic VALUE - // adapter — its first param is dyn, never an object) takes the Node - // positions from param 0 and never receives the stream (the dyn call - // ABI carries no receiver; SEMANTICS.md). - const declared = cbT.params; - const hasThis = declared[0] !== undefined && declared[0].kind === "object"; - if (declared.length === 0) { - throw new InternalCompilerError("emitter bug: stream option callback with no params (frontend must fence)"); - } - const passed: string[] = hasThis - ? ["sc_cb", `(${cType(declared[0]!).trim()})scr_stream_retain(sc_s)`] - : ["sc_cb"]; - const body: string[] = []; - const off = hasThis ? 1 : 0; - const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off; - // "e" (the finished/pipeline callback): ONE Node position — the error. - if (declared.length > full) { - throw new InternalCompilerError(`emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`); - } - for (let i = off; i < declared.length; i++) { - const p = declared[i]!; - const pos = i - off; // 0-based Node signature position - if (kind === "r") { - // read(size) — a dyn-declared size (the JS lane) boxes the number. - passed.push(p.kind === "dyn" ? "scr_dyn_new_num(sc_size)" : "sc_size"); - continue; - } - const isChunkPos = (kind === "w" || kind === "t") && pos === 0; - const isEncPos = (kind === "w" || kind === "t") && pos === 1; - const isErrPos = (kind === "d" || kind === "e") && pos === 0; - const isDonePos = - kind === "e" ? false - : (kind === "w" || kind === "t") ? pos === 2 : (kind === "f" || kind === "l") ? pos === 0 : pos === 1; - // The JS lane's implicitly-any parameters: each position boxes into - // dyn by kind — the chunk as dyn bytes, the encoding as a string, - // the error as the boundary's {name, message} shape (null when - // none), and the completion callback as a CALLABLE dyn whose glue - // dispatches to the runtime's *_done entry (the closure's one cap - // boxes the retained stream). - if (p.kind === "dyn") { - if (isChunkPos) { - passed.push(`scr_dyn_new_buffer(sc_chunk)`); - } else if (isEncPos) { - body.push( - ` ScrStr *sc_encs = scr_str_new("buffer", 6);`, - ` ScrDyn *sc_encd = scr_dyn_new_str(sc_encs);`, - ` scr_str_release(sc_encs);`, - ); - passed.push(`sc_encd`); - } else if (isErrPos) { - // finished/pipeline succeed with UNDEFINED (Node calls the eos - // callback with no arguments); destroy passes null. - passed.push(kind === "e" - ? `sc_err ? scr_dyn_from_error(sc_err) : scr_dyn_retain(scr_dyn_undefined())` - : `sc_err ? scr_dyn_from_error(sc_err) : scr_dyn_new_null()`); - } else if (isDonePos) { - const glue = `scr_stream_done_dyn_${kind}`; - body.push( - ` ScrClosure *sc_dclo = scr_closure_new((void *)&${glue}, 1);`, - ` sc_dclo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace);`, - ` scr_box_set_ref(sc_dclo->caps[0], scr_stream_retain(sc_s));`, - ` ScrDyn *sc_done = scr_dyn_new_func(sc_dclo, &${glue}, ${kind === "t" || kind === "l" ? 2 : 1}, ${JSON.stringify(kind === "t" || kind === "l" ? "(error,data)" : "(error)")}, "callback");`, - ); - passed.push(`sc_done`); - } else { - throw new InternalCompilerError(`emitter bug: stream '${kind}' dyn callback param ${i} has no adapter`); - } - continue; - } - if (isChunkPos) { - passed.push(`scr_bytes_retain(sc_chunk)`); - } else if (isEncPos) { - // Node's encoding for decoded (Buffer) chunks is 'buffer'. - body.push(` ScrStr *sc_enc = scr_str_new("buffer", 6);`); - passed.push(`sc_enc`); - } else if (isErrPos) { - // destroy's error argument: `Error | null` — wrap type-directedly. - // The finished/pipeline callback ("e") may declare `Error | null | - // undefined` (the @types signature); success prefers the undefined - // arm there (Node calls the eos callback with NO arguments). - if (p.kind !== "union") throw new InternalCompilerError("emitter bug: stream destroy err param not a union"); - const def = emitter.unionsById.get(p.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1; - const undefTag = kind === "e" && def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; - const nullTag = def - ? (undefTag >= 0 ? undefTag : def.arms.findIndex((a) => a.kind === "nullT")) - : -1; - if (errTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: stream destroy err union lacks its arms"); - body.push( - ` ScrUnion *sc_eu = sc_err ? scr_union_new_ref(${errTag}, scr_error_retain(sc_err), &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()) : scr_union_retain(${emitter.unitInstanceRef(p.unionId, nullTag)});`, - ); - passed.push(`sc_eu`); - } else if (isDonePos) { - const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position - const doneFn = streamDoneFnFor(emitter, doneKind, p); - body.push( - ` ScrClosure *sc_done = scr_closure_new((void *)&${doneFn}, 1);`, - ` sc_done->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace);`, - ` scr_box_set_ref(sc_done->caps[0], scr_stream_retain(sc_s));`, - ); - passed.push(`sc_done`); - } else { - throw new InternalCompilerError(`emitter bug: stream '${kind}' callback param ${i} has no adapter`); - } - } - const sigParams = ["ScrClosure *", ...declared.map((p) => cType(p).trim())].join(", "); - const unused: string[] = []; - if (declared.length < off + 1) { - if (kind === "r") unused.push(` (void)sc_size;`); - if (kind === "w" || kind === "t") unused.push(` (void)sc_chunk;`); - if (kind === "d" || kind === "e") unused.push(` (void)sc_err;`); - } - if (!hasThis) unused.push(` (void)sc_s;`); /* the done/err adapters may still read it */ - // The call goes through the callback's TRUE signature; a non-void - // result is discarded (refcounted ones released) — Node ignores option - // callback results (`read: () => this.push(null)`). - const retC = cbT.ret.kind === "void" ? "void" : cType(cbT.ret).trim(); - const invoke = `((${retC} (*)(${sigParams}))sc_cb->fn)(${passed.join(", ")})`; - emitter.walkerProtos.push(`static void ${sym}(ScrClosure *sc_cb, ${runtimeParams.join(", ")});`); - emitter.walkerDefs.push( - `static void ${sym}(ScrClosure *sc_cb, ${runtimeParams.join(", ")}) {`, - ...unused, - ...body, - ...(cbT.ret.kind === "void" - ? [` ${invoke};`] - : isRefCounted(cbT.ret) - ? [` ${retC} sc_r = ${invoke};`, ` if (sc_r) ${releaseCallC(cbT.ret, "sc_r")};`] - : [` (void)${invoke};`]), - `}`, - ``, - ); - return sym; -} diff --git a/packages/compiler/src/backend/c/bindings.ts b/packages/compiler/src/backend/c/bindings.ts deleted file mode 100644 index 5a6ab3503..000000000 --- a/packages/compiler/src/backend/c/bindings.ts +++ /dev/null @@ -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});`); -} diff --git a/packages/compiler/src/backend/c/c-emitter.test.ts b/packages/compiler/src/backend/c/c-emitter.test.ts deleted file mode 100644 index a8dd71412..000000000 --- a/packages/compiler/src/backend/c/c-emitter.test.ts +++ /dev/null @@ -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); -}); diff --git a/packages/compiler/src/backend/c/c-emitter.ts b/packages/compiler/src/backend/c/c-emitter.ts deleted file mode 100644 index 6621defbd..000000000 --- a/packages/compiler/src/backend/c/c-emitter.ts +++ /dev/null @@ -1,2214 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; -import { SourceLocations } from "../source-locations.js"; -/* IR → C. Three-address style: every IR expression lands in a fresh C temp. - * Verbose (clang -O2 erases it) but buys three things: short-circuit - * emission is trivially correct, reference counting has one mechanical - * hook point, and the output shape is already close to a CFG lowering. - * - * RC ownership discipline (must match docs/ir.md): - * - every refcounted temp (isRefCounted kinds) holds an owned (+1) reference; - * - varDecl/assign/return/call-argument MOVE that ownership (the temp is - * struck from its release list); everything else borrows; - * - 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 and release them on exit (callers pass +1); - * - `return` first releases pending temps and every in-scope refcounted - * local. - * - * RC dispatch is type-directed: frames and scopes carry {name, type} so a - * release always knows which scr_*_release to call. `isRefCounted` in - * ir.ts is the membership test — no `kind === "string"` checks here. - * - * The generated C is a debugging surface: locals keep their TS names inside - * the mangled form and every statement carries a `source line` comment. - */ -import type { - IrBytesElem, - IrGlobal, - IrRecordShape, - IrExpr, - IrFfiCallbackParamClass, - IrFfiCallbackParam, - IrFfiImport, - IrFfiReleaseParam, - IrFfiReturnClass, - IrFfiValueParamClass, - IrFunction, - IrLocal, - IrModule, - IrStmt, - IrType, - IrUnionDef, - SrcLoc, -} from "../../ir/ir.js"; -import { BYTES_ELEMENT_SIZE, ffiCallbackType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, VOID } from "../../ir/ir.js"; -import { undefinedArmTag } from "../../ir/analysis.js"; -import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import 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 { - mangleAsyncSpawn, - mangleGenSpawn, - mangleClassObj, - mangleField, - mangleGlobal, - mangleFnClosure, - mangleFunction, - mangleLocal, - mangleRawParam, - mangleVtSlot, - mangleWrapper, -} from "../mangle.js"; -import { cCommentText, cFnPtrCast, cType, releaseCallC, cStringLiteral, cDecl, cNumberLiteral } from "./types.js"; -import { computeTraced } from "../cycle-analysis.js"; -import { computeMayThrow } from "./may-throw.js"; -import { unionTruthyHelper, unionEqHelper, unionToStrHelper, unionJoinHelper, jsonWriteHelper, jsonIndentHelper, dynMatchHelper, dynCheckHelper, dynFuncBoxHelper, dynToStrHelper, caughtToDynHelper, toDynHelper, recordKeyGetHelper, recordKeySetHelper } from "./walkers.js"; -import { type StructShape, type VtSlot, ClassMeta, emitStructDefs, vtEntriesFor, vtSlotParams, emitVtableDecls, emitVtableInstances, emitVtAdapterDefs, emitHierarchyClassHelpers, emitClassObjs, emitCtorThunkDefs, errorVtStampLines, emitterVtStampLines, streamVtStampLines, traceAdapterC, traceArgC, boxNewC, arrNewC } from "./shapes.js"; -import { emitAsyncScaffolding, childDataThunkFor, childExitThunkFor, childExitSignalThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, closeBindThunkFor, connectResThunkFor, connectSockThunkFor, closeOverrideWrapFor, cryptoBytesThunkFor, dgramMsgThunkFor, dnsLookupThunkFor, fsRenameThunkFor, genResultThunkFor, netLookupAnswerThunkFor, emitterInvokeThunkFor, streamCbThunkFor, streamDataThunkFor, raceAdapterFor, resolveThunkFor, sniAnswerThunkFor, zlibBytesThunkFor } from "./async.js"; -import { emitNpmEmbedding, islandAdapter, islandTypedAdapter } from "./island.js"; -import { emitFunction, emitBlock, emitStmts, emitStmt, emitTryCatch, emitSwitch, mergeBrace, emitBranchInto, emitCondition } from "./stmts.js"; -import { emitExpr, liveDynRefAdapter as buildLiveDynRefAdapter, type StreamTypedRefAdapter } from "./exprs.js"; -import { emitLibraryIdentityLines } from "../library-identity-markers.js"; - -export interface CEmitOptions { - /** Exact frontend sources for dev-build #line directives. */ - debugSources?: ReadonlyMap; - /** Library archive assembly may move the volatile identity getters into a - * separate translation unit. Public/direct emission keeps them by default. */ - emitLibraryIdentity?: boolean; -} - -export function emitCModule( - mod: IrModule, - sourceText?: string, - options: CEmitOptions = {}, -): string { - return new CEmitter(scalarizeNumericRecords(mod), sourceText, options).emit(); -} - -/** Output chunks avoid the host's single-string size limit when a compiler - * contains both emitters. Keep individual emitted lines intact, including - * UTF-16 surrogate pairs. Joining these preserves emitCModule's exact bytes. */ -export function emitCModuleChunks( - mod: IrModule, - sourceText?: string, - options: CEmitOptions = {}, -): string[] { - const lines = new CEmitter(scalarizeNumericRecords(mod), sourceText, options).emitLines(); - const chunks: string[] = []; - let chunk = ""; - let first = true; - for (const line of lines) { - if (!first) chunk += "\n"; - first = false; - chunk += line; - if (chunk.length >= 64 * 1024) { chunks.push(chunk); chunk = ""; } - } - if (chunk !== "") chunks.push(chunk); - return chunks; -} - -// Box construction moved onto CEmitter (boxNewC method): obj-kind boxes now -// also carry the payload type's trace entry point, which is type-directed -// through the emitter's cycle analysis. - -/** A declared C value with its IR type — the unit frames and scopes track - * so releases can be type-directed. */ -export interface Temp { - name: string; - type: IrType; -} - -type TruthyPointerType = Extract -}>; - -/** A scope entry: a refcounted local, either held directly or through a - * capture box (boxed locals release their BOX; the box frees its contents). */ -export interface ScopeEntry extends Temp { - boxed?: boolean; -} - -function ffiNativeTypeC( - cls: IrFfiCallbackParamClass | IrFfiValueParamClass | IrFfiReturnClass, -): string { - switch (cls) { - case "f64": - return "double"; - case "f32": - return "float"; - case "bool": - case "u8": - return "uint8_t"; - case "i8": - return "int8_t"; - case "u16": - return "uint16_t"; - case "i16": - return "int16_t"; - case "u32": - return "uint32_t"; - case "i32": - return "int32_t"; - case "i64": return "int64_t"; - case "u64": return "uint64_t"; - case "pointer": return "void *"; - case "cstring": - return "const char *"; - case "string": - case "bytes": - case "mutable-bytes": - throw new InternalCompilerError(`emitter bug: span class '${cls}' has no scalar C type`); - case "void": - return "void"; - } -} - -function ffiCallbackNativeParamsC( - callback: IrFfiCallbackParam["callback"] | IrFfiReleaseParam["callback"], - named: boolean, -): string[] { - return callback.params.flatMap((param, i): string[] => { - if (isFfiContextParam(param)) return [`void *${named ? "sc_ctx" : ""}`.trim()]; - if (param === "string" || param === "bytes") { - return [ - `const uint8_t *${named ? `sc_a${i}` : ""}`.trim(), - `size_t${named ? ` sc_a${i}_len` : ""}`, - ]; - } - return [`${ffiNativeTypeC(param)}${named ? ` sc_a${i}` : ""}`]; - }); -} - -function ffiCallbackPointerTypeC( - callback: IrFfiCallbackParam["callback"] | IrFfiReleaseParam["callback"], -): string { - const ret = ffiNativeTypeC(callback.returns); - const params = ffiCallbackNativeParamsC(callback, false); - return `${ret} (*)(${params.length > 0 ? params.join(", ") : "void"})`; -} - -function ffiCallbackDummyC(callback: IrFfiCallbackParam["callback"]): string { - return callback.returns === "void" ? "" : "0"; -} - -export class CEmitter { - private readonly debug: SourceLocations | null; - sourceLoc: SrcLoc | null = null; - readonly lines: string[] = []; - indent = 0; - tempCounter = 0; - /** Interned string literals: UTF-8 text → static symbol name. */ - readonly literals = new Map(); - /** Kind-specialized typed-array access helpers actually used by this - * program. Keeping these in the generated TU means unrelated binaries do - * not pay even debug/link metadata for byte-loop fast paths. */ - readonly bytesElementHelpers = new Set<`${"get" | "set"}:${IrBytesElem}:${"f64" | "u64"}`>(); - /** Interned unit-armed union instances: "unionId:tag" → static symbol. - * A unit arm (undefined/null) has no payload, so every instance of one - * (union, tag) pair is identical — ONE immortal (rc == SIZE_MAX) static - * serves them all. Immortals are skipped by retain/release and by the - * cycle collector's child filter (SCR_CYC_SKIP), so the missing cycle - * header is fine even when a traced container points at one. */ - readonly unitInstances = new Map(); - /** Interned regex literals: "/" → static symbol. One - * immortal (rc == SIZE_MAX) ScrRegex per distinct (pattern, flags) pair — - * the bytecode slot starts NULL and the runtime compiles it lazily on - * first use. The source/flags strings ride the ordinary literal table. */ - readonly regexInstances = new Map(); - /** Interned tagged-template strings objects: per-site key → symbol + - * cooked spans. One immortal (rc == SIZE_MAX) ScrArr of interned string - * literals per template-literal SITE — the spec's per-occurrence - * identity: the same site evaluated twice hands the tag the same array, - * two sites never share even with identical text. */ - readonly templateStringsInstances = new Map(); - /** Class objects (classes as first-class values): className → static - * symbol. One immortal ScrClassObj per class some classRef names — - * preorder interval baked as constants, the .name string in the literal - * table, and a per-class construct thunk (void *sc_ct_*) newValue - * dispatches through. Registered during body emission (the regex - * pattern); the statics and thunks are assembled around the bodies. */ - readonly classObjs = new Map(); - /** Statement frames own expression temps and sequence locals until cleanup. */ - frames: ScopeEntry[][] = []; - /** Stack of scopes: refcounted locals (with types) declared in each. */ - scopes: ScopeEntry[][] = []; - /** The function being emitted: local table (for boxedness) and whether - * each local arrived through the closure environment (env captures are - * borrowed — never declared, never released here). */ - currentLocals = new Map(); - captureIds = new Set(); - /** Canonical byte-loop induction locals currently represented by an - * unsigned integer shadow. Ordinary number reads widen the shadow back to - * f64; direct byte indices consume it without a conversion round trip. */ - integerLoopBindings = new Map(); - integerRanges: IntegerRanges = new Map(); - /** Declared functions referenced as values: each needs an env-signature - * wrapper + an interned immortal closure (so `f === f` holds). */ - readonly fnValues = new Set(); - /** Emitted ref-kind resolve thunks for new Promise, interned per inner - * typeKey → thunk symbol. */ - readonly resolveThunks = new Map(); - readonly genResThunks = new Map(); - /** Emitted child exit adapters, interned per union id (childExitThunkFor). */ - readonly childExitThunks = new Map(); - /** Emitted child-stream data adapters, interned per union id - * (childDataThunkFor). */ - readonly childDataThunks = new Map(); - /** Emitted bound-close adapters, interned per union id - * (closeBindThunkFor). */ - readonly closeBindThunks = new Map(); - /** Emitted close-override wrappers, interned per (union id, ret kind) - * (closeOverrideWrapFor). */ - readonly closeOverrideWraps = new Map(); - /** Emitted dgram message adapters, interned per rinfo shape id - * (dgramMsgThunkFor). */ - readonly dgramMsgThunks = new Map(); - /** Emitted dns.lookup callback adapters, interned per union id + param - * count (dnsLookupThunkFor). */ - readonly dnsLookupThunks = new Map(); - /** Emitted fs.rename callback adapters, interned per callback type. */ - readonly fsRenameThunks = new Map(); - /** Emitted SNI answer-closure thunks, interned per cb func-type key - * (sniAnswerThunkFor). */ - readonly sniAnswerThunks = new Map(); - /** Emitted net.connect lookup answer thunks, interned per cb func-type - * key (netLookupAnswerThunkFor). */ - readonly netLookupAnswerThunks = new Map(); - /** Emitted EventEmitter listener invoke adapters, interned per listener - * func-type key (emitterInvokeThunkFor). */ - readonly emitterInvokeThunks = new Map(); - /** Emitted stream option-callback invoke adapters (read/write/final/ - * destroy/transform/flush — the leading-`this` closures), interned per - * (kind, callback func-type) key (streamCbThunkFor). */ - readonly streamCbThunks = new Map(); - /** Emitted stream completion-callback closure fns (the `callback` a - * user's write/final/destroy/transform/flush receives and calls), - * interned per (kind, done func-type) key (streamDoneFnFor). */ - readonly streamDoneFns = new Map(); - /** Emitted CONNECT-listener union-socket adapters, interned per cb - * func-type key (connectSockThunkFor). */ - readonly connectSockThunks = new Map(); - /** Emitted Promise.race fulfillment adapters, interned per - * `entryInner=>resultInner` typeKey pair (raceAdapterFor). */ - readonly raceThunks = new Map(); - /** setTimeout appeared somewhere: main must run the event loop even in - * programs with no async functions. */ - usesTimers = false; - readonly fnByName = new Map(); - /** Manifest-bound native imports, used by ffiCall emission. */ - readonly ffiByName = new Map(); - /** One C-ABI trampoline per manifest callback entry. Raw function - * pointers additionally get a distinct TLS context slot, so two - * callbacks with the same signature never alias each other's closure. */ - readonly ffiCallbackAdapters: Map; - /** Module-level constant consulted per ffiCall: with a retained - * descriptor anywhere in the manifest, every native call is a - * pending-exception checkpoint (may-throw derives the same fact from - * the same helper). */ - readonly ffiHasRetainedCallback: boolean; - readonly ffiHasForeignCallback: boolean; - readonly globalsById = new Map(); - readonly constantNumericTables: ReadonlyMap; - readonly unionsById = new Map(); - /** Active optional-chain bind temps, by chain id (chainRecv reads). */ - readonly chainTemps = new Map(); - readonly recordsById = new Map(); - /** Record shapes whose bodies emitted recordClone. Filled while function - * bodies emit, before emitStructDefs assembles per-shape helpers. */ - readonly recordCloneShapes = new Set(); - /** Type-directed JSON walkers, interned per typeKey — one serializer per - * type used in jsonStringify position (sc_jw_*), one match predicate - * (sc_dm_*) and one checked builder (sc_dc_*) per type used in dynCheck - * position. Emitted as prototypes + definitions after the struct block - * (they reference struct types, per-shape RC helpers, and each other, so - * the prototypes make definition order irrelevant). */ - readonly jsonWriters = new Map(); - /** The pretty-print re-indenter (type-independent, interned once): - * emitted on the first `JSON.stringify(v, null, space)` site. */ - jsonIndentFn: string | null = null; - /** The dyn ToString pair (type-independent, interned once): emitted on - * the first String(unknown) / `${unknown}` site. */ - dynToStrFn: string | null = null; - /** The caught→dyn converter (type-independent, interned once): emitted - * on the first catch binding flowing into an `unknown` slot. */ - caughtToDynFn: string | null = null; - /** Interned per-union helpers: unionId → emitted function name. */ - readonly unionTruthyFns = new Map(); - readonly unionEqFns = new Map(); - readonly unionToStrFns = new Map(); - readonly unionJoinFns = new Map(); - readonly dynMatchers = new Map(); - readonly dynBuilders = new Map(); - /** Static→dyn converters (sc_td_*), per typeKey; dynamic-keyed record - * read helpers (sc_rkg_*), per shapeId|result typeKey; dynamic-keyed - * write helpers (sc_rks_*), per shapeId. */ - readonly toDynFns = new Map(); - /** ReadableStream.from typed-array element adapters, per element - * typeKey. The runtime retains the original array and calls one of these - * for each pull, preserving iterator-time reads without teaching the - * runtime the compiler's program-specific record/union layouts. */ - readonly streamFromArrayAdapters = new Map(); - /** Identity-preserving static→dyn capsules for program classes and Web - * APIs whose values remain directly observable. */ - readonly liveDynRefAdapters = new Map< - string, - { snapshot: string; commit: string } - >(); - /** Runtime-arm dispatchers for live references whose static type is a - * union. Mutable arms become typed capsules; scalar/unit arms retain the - * ordinary static-to-dyn conversion. */ - readonly liveDynUnionRefAdapters = new Map(); - readonly dynPromiseAdapters = new Map(); - /** The checked-dynamic function boundary's per-signature helpers (see - * walkers.ts): call thunks (sc_dfk_*), box builders (sc_dfb_*), - * and dynCheck adapters (sc_dfa_*), each per func typeKey. */ - readonly dynFuncThunks = new Map(); - readonly dynFuncBoxes = new Map(); - readonly dynFuncAdapters = new Map(); - /** dyn-promise settle adapters (sc_pda_*), per INNER typeKey: convert a - * typed fulfillment payload into the boxed destination's dyn payload - * (scr_dyn_new_promise_adapting's callback — toDynHelper's promise arm). */ - readonly promiseDynAdapters = new Map(); - readonly recordKeyGetFns = new Map(); - readonly unionWidenFns = new Map(); - readonly recordKeySetFns = new Map(); - readonly walkerProtos: string[] = []; - readonly walkerDefs: string[] = []; - /** Island host-call adapters, interned per (arity, void-ness): the one - * uniform shape scr_jsval_from_closure calls — unpack the cell array, - * call the closure through its real ABI (which CONSUMES its params, so - * each cell is retained in), return the +1 result cell or NULL for void. - * Definitions ride walkerDefs (self-contained over the closure ABI). */ - readonly islandAdapters = new Map(); - /** TYPED island host-call adapters, interned per full signature (param - * typeKeys + return classification): each incoming engine argument - * converts to the param's static type through the exit machinery before - * the closure runs — see islandTypedAdapter (island.ts). */ - readonly islandTypedAdapters = new Map(); - /** Enclosing break/continue targets. An unlabeled `break` binds to the - * innermost loop-or-switch entry (labeled BLOCK entries are skipped); an - * unlabeled `continue` searches inward-out for the innermost LOOP, - * skipping switches and blocks. A LABELED jump binds to the entry whose - * `labels` contains its label: `break lbl` jumps to the target's - * endLabel (loops allocate one lazily via `usedEnd` — a C `break` only - * exits the innermost loop), `continue lbl` to the target loop's - * continueLabel. Loops: `continueLabel` is null when C `continue` is - * correct (unlabeled while/forOf); for/do-while loops need a goto label - * so `continue` still runs the update/condition, and LABELED loops of - * every shape allocate one up front (a labeled continue from a nested - * loop needs a goto). Switches are emitted as goto chains — never as C - * `switch` — so a C `break`/`continue` inside an emitted switch region - * still binds to the enclosing C loop; a break targeting the switch - * itself jumps to `endLabel` instead. Labeled blocks carry only an - * endLabel (`break lbl` is the only jump that can target them). - * `scopeDepth` = scopes.length at entry — a break/continue releases - * every scope pushed after that before jumping. `frameDepth` = - * frames.length at entry: statements the jump exits may still hold - * pending refcounted temps in their frames (a switch's discriminant, - * most notably), whose normal end-of-statement releases sit on the - * fall-through path the jump bypasses — the jump releases every frame - * pushed after the target's own before jumping. */ - jumpTargets: ( - | { - kind: "loop"; - continueLabel: string | null; - usedContinue: boolean; - endLabel: string | null; - usedEnd: boolean; - labels?: string[] | undefined; - scopeDepth: number; - frameDepth: number; - finallyDepth: number; - } - | { kind: "switch"; endLabel: string; usedEnd: boolean; labels?: string[] | undefined; scopeDepth: number; frameDepth: number; finallyDepth: number } - | { kind: "block"; endLabel: string; usedEnd: boolean; labels: string[]; scopeDepth: number; frameDepth: number; finallyDepth: number } - )[] = []; - labelCounter = 0; - readonly returnTypeByFn = new Map(); - lineStarts: number[] | null = null; - /** May-throw analysis results (computeMayThrow): pending-exception checks - * are emitted only after calls that can actually raise. */ - readonly mayThrow: Set; - readonly indirectMayThrow: boolean; - /** Enclosing try contexts, innermost last — the compile-time analogue of - * the jump-target stack for UNWINDING: a pending check (or `throw`) inside - * a try releases frames/scopes down to the recorded depths and jumps to - * `label` (the catch, or the exception-path finally) instead of returning - * out of the function. Purely compile-time: entering a try emits no code. */ - tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = - []; - /** Enclosing try-with-finally regions, innermost last. Returns use the - * central pending-return copies; break/continue inline the crossed bodies - * with the same truncated ownership and exception context as LLVM. */ - finallyStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number; tryDepth: number; body: IrStmt[] }[] = - []; - /** Scope entry protecting sc_pret while a pending-return finally copy - * runs. A return in that copy replaces the old completion: it evaluates - * first, releases the old slot, then removes this protection from its - * own unwind path before dispatching. */ - pendingReturnScopeIndex: number | null = null; - /** Return type of the function being emitted — the unwind path returns a - * dummy of this type (never read: callers check the pending flag first). */ - currentReturnType: IrType = VOID; - /** The generator channels of the function being emitted (null outside - * generator bodies): yieldExpr emission reads them, and emitTryCatch's - * catch prologue emits the GENRET sentinel re-unwind exactly here. */ - currentGenerator: { yieldT: IrType; nextT: IrType } | null = null; - /** Cycle-capable shapes ("object:" / "record:") and unions - * (unionId): instances get a cycle header + emitted trace/teardown, and - * fields/payloads of these types are visited by container traces. - * Computed as a greatest fixpoint in the constructor — see there. */ - readonly tracedShapes = new Set(); - readonly tracedUnions = new Set(); - /** The class graph (single inheritance): base/children links, hierarchy - * membership (extends anywhere ⇒ vtable word + dynamic release), the - * whole-program preorder numbering behind O(1) instanceof, and the - * per-hierarchy virtual slot lists (root classes only). Computed once in - * the constructor. */ - readonly classMeta = new Map(); - /** Method names with at least one may-throw implementation: a - * virtualCall's pending check keys on this (same over-approximation as - * computeMayThrow's callee cover). */ - readonly mayThrowMethods = new Set(); - /** Vtable slot adapters to define after the function signatures (they - * call sc_f_* bodies): dedupe key "implClass.method". */ - readonly vtAdapters = new Map(); - - constructor( - readonly mod: IrModule, - sourceText?: string, - private readonly options: CEmitOptions = {}, - ) { - this.debug = options.debugSources === undefined ? null : new SourceLocations(options.debugSources); - this.constantNumericTables = findConstantNumericTables(mod); - this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []); - this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []); - this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []); - for (const fn of mod.functions) { - this.returnTypeByFn.set(fn.name, fn.returnType); - this.fnByName.set(fn.name, fn); - } - for (const entry of mod.ffiImports ?? []) { - this.ffiByName.set(entry.name, entry); - } - const mt = computeMayThrow(mod); - this.mayThrow = mt.fns; - this.indirectMayThrow = mt.indirect; - for (const g of mod.globals ?? []) this.globalsById.set(g.id, g); - for (const u of mod.unions ?? []) this.unionsById.set(u.id, u); - for (const r of mod.records ?? []) this.recordsById.set(r.id, r); - // The class graph. Link base/children, number the forest in preorder - // (roots and children in module class order — deterministic), and - // compute each hierarchy's virtual slots: a class's method gets a slot - // iff no ancestor declares it (root-most) AND some strict descendant - // redeclares it — never-overridden methods stay direct calls - // everywhere (whole-program devirtualization). - for (const cls of mod.classes ?? []) { - const meta = new ClassMeta(cls); - this.classMeta.set(cls.name, meta); - } - for (const meta of this.classMeta.values()) { - if (meta.def.base === undefined) continue; - const base = this.classMeta.get(meta.def.base); - if (!base) throw new InternalCompilerError(`emitter bug: undeclared base class ${meta.def.base}`); - meta.base = base; - base.children.push(meta); - } - let preCounter = 0; - const number = (meta: ClassMeta, root: ClassMeta): void => { - meta.root = root; - meta.pre = preCounter++; - for (const c of meta.children) number(c, root); - meta.post = preCounter - 1; // max pre in the subtree (inclusive) - }; - for (const meta of this.classMeta.values()) { - if (meta.base === null) number(meta, meta); - // The runtime emitter class is ALWAYS a hierarchy member: ScrEmitter - // carries its vtable word whether or not the program subclasses it. - meta.hierarchy = meta.base !== null || meta.children.length > 0 || - meta.def.name === RUNTIME_EMITTER_CLASS; - } - const declares = (m: ClassMeta, method: string): boolean => - m.def.methods?.includes(method) ?? false; - const declaredBelow = (m: ClassMeta, method: string): boolean => - m.children.some((c) => declares(c, method) || declaredBelow(c, method)); - const collectSlots = (m: ClassMeta, root: ClassMeta, seen: Map): void => { - for (const method of m.def.methods ?? []) { - let inherited = false; - for (let a = m.base; a; a = a.base) inherited = inherited || declares(a, method); - if (!inherited && declaredBelow(m, method)) { - let fn = this.fnByName.get(`%${m.def.name}.${method}`); - if (!fn && m.def.abstractMethods?.includes(method)) { - // An ABSTRACT declarer has no function; the slot's ABI - // signature comes from any concrete descendant implementation - // (the frontend's override exactness makes them identical). - // No concrete implementation anywhere in the subtree means no - // instance can dispatch the slot (only abstract classes - // declare it, and abstract classes never instantiate) — skip. - const findImpl = (c: ClassMeta): IrFunction | undefined => { - for (const child of c.children) { - const f = declares(child, method) && !child.def.abstractMethods?.includes(method) - ? this.fnByName.get(`%${child.def.name}.${method}`) - : undefined; - const found = f ?? findImpl(child); - if (found) return found; - } - return undefined; - }; - fn = findImpl(m); - if (!fn) continue; - } - if (!fn) throw new InternalCompilerError(`emitter bug: missing method function %${m.def.name}.${method}`); - // Sibling branches can each own a slot for the same method name - // (each root-most in its own subtree — mixin layers make this - // routine): the member name disambiguates by occurrence. - const occurrence = seen.get(method) ?? 0; - seen.set(method, occurrence + 1); - root.slots.push({ method, declarer: m, fn, member: mangleVtSlot(method, occurrence) }); - } - } - for (const c of m.children) collectSlots(c, root, seen); - }; - for (const meta of this.classMeta.values()) { - if (meta.base === null && meta.hierarchy) collectSlots(meta, meta, new Map()); - } - for (const cls of mod.classes ?? []) { - for (const m of cls.methods ?? []) { - if (this.mayThrow.has(`%${cls.name}.${m}`)) this.mayThrowMethods.add(m); - } - } - const traced = computeTraced(mod); - for (const shape of traced.shapes) this.tracedShapes.add(shape); - for (const union of traced.unions) this.tracedUnions.add(union); - if (sourceText !== undefined) { - this.lineStarts = [0]; - for (let i = 0; i < sourceText.length; i++) { - if (sourceText.charAt(i) === "\n") this.lineStarts.push(i + 1); - } - } - } - - emit(): string { return this.emitLines().join("\n"); } - - emitLines(): string[] { - const body: string[] = []; - // Async-generator spawn wrappers need their type-directed result - // builders even when user code only creates and drops the iterator. - // Register them before the unit-instance table flushes below. - for (const fn of this.mod.functions) { - if (!fn.async || fn.generator === undefined) continue; - genResultThunkFor( - this, - { - kind: "generator", - async: true, - yieldT: fn.generator.yieldT, - retT: fn.returnType, - nextT: fn.generator.nextT, - }, - fn.generator.resultType, - ); - } - // Function bodies are emitted first (into this.lines) so the literal - // table is complete; the file is then assembled around them. - for (const fn of this.mod.functions) { - this.emitFunction(fn); - // Large compiler functions can emit more lines than JavaScript's - // argument-count limit, particularly with sanitizer cleanup paths. - for (const line of this.lines) body.push(line); - this.lines.length = 0; - } - - const out: string[] = [ - `/* Generated by scriptc from ${this.mod.sourceFile}. Do not edit. */`, - `#include "scr_runtime.h"`, - `#include `, - `#include `, - `#include `, - ``, - ]; - out.push(...this.emitBytesElementHelpers()); - for (const table of this.constantNumericTables.values()) { - out.push( - `static const double ${table.symbol}[] = { ${table.values.map(cNumberLiteral).join(", ")} };`, - `static inline double ${table.symbol}_get(const ScrArr *a, double i) {`, - // Range checks precede the conversion: NaN, infinities and large - // numbers must never reach a C float-to-integer conversion. - ` if (a != NULL && i >= 0.0 && i < ${table.values.length}.0) {`, - ` size_t index = (size_t)i;`, - ` if ((double)index == i) return ${table.symbol}[index];`, - ` }`, - ` return scr_arr_get_number(a, i);`, - `}`, ``, - ); - } - // Struct defs render into their own buffer BEFORE the unit-instance - // table flushes: class newFns point undefined-armed union fields at - // interned unit instances (fields start as JS's undefined, not NULL), - // so the instances they intern must both exist in the map by flush - // time and be DEFINED earlier in the file than the newFn bodies. - const structDefs: string[] = []; - this.emitStructDefs(structDefs); - for (const [text, sym] of this.literals) { - const bytes = Buffer.from(text, "utf8"); - out.push( - `static struct { size_t rc; size_t len; size_t cap; char data[${bytes.length + 1}]; } ${sym} =`, - ` { SIZE_MAX, ${bytes.length}, ${bytes.length}, ${cStringLiteral(bytes)} };`, - ); - } - if (this.literals.size > 0) out.push(""); - for (const [key, sym] of this.unitInstances) { - // One immortal instance per unit-armed (union, tag): tag set, payload - // slot and RC entry points zero — retain/release/collector all skip - // rc == SIZE_MAX, so these never join the RC audit or a trace walk. - const [unionId, tag] = key.split(":"); - out.push( - `static ScrUnion ${sym} = { .rc = SIZE_MAX, .tag = ${tag} }; /* ${unionId} unit arm */`, - ); - } - if (this.unitInstances.size > 0) out.push(""); - for (const line of structDefs) out.push(line); // program-sized: never spread - for (const [key, sym] of this.regexInstances) { - // One immortal ScrRegex per (pattern, flags) literal, pointing at the - // interned source/flags strings. `.bc` starts NULL: the runtime - // compiles the pattern lazily on first use and caches it here (and - // frees it at exit), so unexecuted regexes cost nothing. NOT const — - // the bc slot mutates. - const sep = key.indexOf("/"); - const flags = key.slice(0, sep); - const pattern = key.slice(sep + 1); - const src = this.internLiteral(pattern); - const fl = this.internLiteral(flags); - const safe = cCommentText(`/${pattern}/${flags}`); - out.push( - `static ${this.mod.lib?.threadInstances === true ? "_Thread_local " : ""}ScrRegex ${sym} = { .rc = SIZE_MAX, .source = (ScrStr *)&${src}, ` + - `.flags = (ScrStr *)&${fl}, .bc = NULL }; /* ${safe} */`, - ); - } - if (this.regexInstances.size > 0) out.push(""); - for (const [, { sym, cooked }] of this.templateStringsInstances) { - // One immortal ScrArr per tagged-template site: the data slots point - // at the interned cooked-string literals (address constants — fully - // static, no lazy init; reads retain immortal strings, a no-op). - // len == cap and nothing ever mutates it (TemplateStringsArray is - // ReadonlyArray, tsc rejects the mutating spellings). - const slots = cooked.map((s) => `(void *)&${this.internLiteral(s)}`).join(", "); - out.push( - `static void *${sym}_data[${cooked.length}] = { ${slots} };`, - `static ScrArr ${sym} = { .rc = SIZE_MAX, .len = ${cooked.length}, .cap = ${cooked.length}, ` + - `.elem = SCR_ELEM_STR, .elem_retain = NULL, .elem_release = NULL, .elem_trace = NULL, ` + - `.data = (uint64_t *)${sym}_data };`, - ); - } - if (this.templateStringsInstances.size > 0) out.push(""); - const embedded = this.mod.embedded; - emitNpmEmbedding(this, out); - const globals = this.mod.globals ?? []; - // Thread-instanced library state (abi.instance_per_thread): module - // globals, run-once guards, and the regex literal caches below live in - // thread-local storage — one full instance per embedder thread, - // matching the runtime objects compiled with -DSCR_THREAD_INSTANCES. - // Immutable interned data (string literals, unit arms, template - // arrays, vtables) stays shared. - const tl = this.mod.lib?.threadInstances === true ? "_Thread_local " : ""; - for (const g of globals) { - // File-scope statics: zero/NULL-initialized, assigned by %init - // functions, released (refcounted ones) before the RC audit runs. - out.push(`static ${tl}${cDecl(g.type, mangleGlobal(g.id))}; /* ${g.name} */`); - } - if (globals.length > 0) out.push(""); - // Outbound native FFI declarations. string/bytes each expand from one - // scriptc value to a borrowed pointer+length pair. Format-2 callbacks - // and their independently positioned contexts are exact pointer slots. - // Library callback channels reuse ffiCall IR but are NOT direct symbol - // imports: their profile names are registration keys and TypeScript - // bindings only, and call sites dispatch through the runtime slot. Do - // not emit extern declarations for them (besides inventing undefined - // symbols, a valid TS channel name such as `int` is a C keyword). - const libraryCallbackNames = new Set(this.mod.lib?.callbacks?.map((cb) => cb.name) ?? []); - const directFfiImports = (this.mod.ffiImports ?? []).filter( - (entry) => !libraryCallbackNames.has(entry.name) && entry.callbackOperation === undefined, - ); - for (const entry of directFfiImports) { - const params = entry.params.flatMap((param): string[] => { - if (isFfiCallbackParam(param) || isFfiReleaseParam(param)) { - return [ffiCallbackPointerTypeC(param.callback)]; - } - if (isFfiContextParam(param)) return ["void *"]; - if (param === "mutable-bytes") return ["uint8_t *", "size_t"]; - return param === "string" || param === "bytes" - ? ["const uint8_t *", "size_t"] - : [ffiNativeTypeC(param)]; - }); - const ret = ffiNativeTypeC(entry.returns); - out.push(`extern ${ret} ${entry.symbol}(${params.length > 0 ? params.join(", ") : "void"});`); - } - if (directFfiImports.length > 0) out.push(""); - for (const fn of this.mod.functions) out.push(this.signature(fn) + ";"); - this.emitFfiCallbackDefs(out); - // Class objects (classes as values): construct-thunk prototypes plus - // the immortal statics that take their addresses — after the function - // signatures (the thunks call sc_new_*/the constructors), before - // anything that references &sc_co_*. - emitClassObjs(this, out); - // Vtable slot adapters: prototyped with the vtables (emitStructDefs), - // defined here where the method bodies they call are declared. - this.emitVtAdapterDefs(out); - // Wrappers + interned closures for declared functions used as values. - // Placed after the forward declarations (they call sc_f_*) and before - // the bodies (which reference &sc_fc_*). - this.emitAsyncScaffolding(out); - for (const name of this.fnValues) { - const fn = this.fnByName.get(name)!; - const params = ["ScrClosure *sc_env", ...fn.params.map((p) => cDecl(p.type, mangleLocal(p.localId)))]; - const call = `${this.callTargetC(name)}(${fn.params.map((p) => mangleLocal(p.localId)).join(", ")})`; - const retType = fn.generator ? "ScrGen *" : fn.async ? "ScrPromise *" : cType(fn.returnType); - out.push( - ``, - `static ${retType}${retType.endsWith("*") ? "" : " "}${mangleWrapper(name)}(${params.join(", ")}) {`, - ` (void)sc_env;`, - fn.returnType.kind === "void" && !fn.async && !fn.generator ? ` ${call};` : ` return ${call};`, - `}`, - `static struct { size_t rc; void *fn; size_t ncaps; ScrBox *props; } ${mangleFnClosure(name)} =`, - ` { SIZE_MAX, (void *)&${mangleWrapper(name)}, 0, NULL };`, - ); - } - // Construct-thunk definitions (prototyped with the class objects - // above): they call sc_new_* and the constructors, both declared. - emitCtorThunkDefs(this, out); - // Type-directed JSON walkers (jsonStringify serializers, dynCheck - // matchers/builders), interned per type during body emission above. - if (this.walkerProtos.length > 0) { - out.push(""); - for (const line of this.walkerProtos) out.push(line); - out.push(""); - for (const line of this.walkerDefs) out.push(line); - } - // Loop-appended, never spread: `body` scales with the PROGRAM (a large - // embedded graph emits hundreds of thousands of lines), and a spread - // push passes every line as a call argument — the engine's stack - // overflows long before memory matters. - out.push(""); - for (const line of body) out.push(line); - if (this.mod.lib !== undefined) { - // LIBRARY mode: no main(), no scr_init/scr_lib_init, no event - // loop — the profile-declared external symbols instead. Everything - // above is unchanged (still all internal linkage). - this.emitLibEntries(out, globals); - return out; - } - const refGlobals = globals.filter((g) => isRefCounted(g.type)); - // 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. Only emitted - // when the dispatch unit is even linked (defineProps is the only - // writer). - const fnValueProps = moduleUsesDynInvoke(this.mod) ? [...this.fnValues] : []; - if (refGlobals.length > 0 || fnValueProps.length > 0) { - out.push(`static void sc_release_globals(void) {`); - for (const g of refGlobals) { - out.push(` ${releaseCallC(g.type, mangleGlobal(g.id))};`); - } - for (const name of fnValueProps) { - out.push( - ` if (${mangleFnClosure(name)}.props) { scr_box_release(${mangleFnClosure(name)}.props); ${mangleFnClosure(name)}.props = NULL; }`, - ); - } - out.push(`}`, ``); - } - const asyncEntry = this.fnByName.get(this.mod.entry)?.async === true; - const hasAsync = this.mod.functions.some((f) => f.async); - // Generator programs run the loop too (an empty pass when nothing is - // pending): its exit accounting notes still-suspended generator - // fibers as abandoned, so the RC audit downgrades exactly like the - // async loop-exhaustion story. - const hasGenerators = this.mod.functions.some((f) => f.generator !== undefined); - // Embedded npm code can leave island promise chains pending when %main - // returns (a package function's async work) — the loop's io hook - // drains the engine's job queue at quiescence, so npm-importing - // programs always run the loop, like Node always runs its own. - const usesIsland = embedded !== undefined && embedded.modules.length > 0; - const snapshotsTlsCa = - moduleUsesTls(this.mod) || - moduleUsesTlsCa(this.mod) || - moduleEmbedsBuiltin(this.mod, "node:https") || - moduleEmbedsBuiltin(this.mod, "node:tls"); - // A pending module root normally selects Node's exit status 13, but - // an already-failed node:test run or an embedded process.exitCode has - // higher precedence. Keep this expression shared with the ordinary - // successful epilogue so both paths consult the same program verdict. - const usesNodeTest = moduleUsesNodeTest(this.mod); - const programExitUsesIsland = !usesNodeTest && usesIsland; - const programExitCode = usesNodeTest - ? "scr_test_exit_code()" - : usesIsland - ? "scr_island_exit_code()" - : "scr_exit_code_hint_get()"; - // Exit listeners can read MODULE GLOBALS directly (test/common's - // runCallChecks over its mustCallChecks ledger — an interned top-level - // closure, no capture boxes keeping anything alive), so they must run - // BEFORE sc_release_globals — the atexit half alone would fire after - // main freed them (observed use-after-free). scr_run_exit_listeners - // is idempotent (scr_exit_ran), so the atexit becomes a no-op; the - // code argument is the hint the failure reporters maintain — exactly - // what the atexit path would have passed. With a retained FFI - // descriptor the inline call is required even with nothing to - // release: listeners must beat the atexit FFI ledger sweep (a - // listener may legitimately release or pump a registration), and - // only the inline call orders ahead of every atexit handler. Plain - // event programs with neither keep the atexit path, so their - // listener timing is unchanged. - const needsRelease = refGlobals.length > 0 || fnValueProps.length > 0; - const runExitListeners = - moduleUsesProcessEvents(this.mod) && (needsRelease || this.ffiHasRetainedCallback) - ? "scr_run_exit_listeners((double)scr_exit_code_hint_get()); " - : ""; - const exitCleanup = `${runExitListeners}${needsRelease ? "sc_release_globals(); " : ""}`; - const releaseGlobals = needsRelease - ? ` ${runExitListeners}sc_release_globals();` - : runExitListeners !== "" - ? ` ${runExitListeners.trim()}` - : ` /* no refcounted globals */`; - const uncaught = (indent: string, releaseTop = false, handle = false) => [ - `${indent}if (scr_exc_pending()${handle ? " && !scr_exc_handle_uncaught(false)" : ""}) {`, - `${indent} scr_exc_print_uncaught();`, - `${indent} ${releaseTop ? "scr_promise_release(sc_top); " : ""}` + - `${exitCleanup}return scr_exit_code_hint_get();`, - `${indent}}`, - ]; - out.push( - // Real argc/argv feed the library's interned process.argv (see - // scr_lib_init — lazy, so argv-free programs allocate nothing). - `int main(int argc, char **argv) {`, - ` scr_init();`, - // The builtin error classes' preorder intervals are program-dependent - // (they share this module's class-forest numbering, so instanceof and - // the uncaught printer's Error-range test agree between runtime-made - // and compiled error objects) — stamp them before any code runs. - ...this.errorVtStampLines(), - // The runtime emitter vtable's interval, when the program touches - // node:events (the class def rides the module exactly then). - ...emitterVtStampLines(this), - // The runtime stream vtables' intervals, when the program touches - // node:stream (the emitter story — instanceof and dynamic teardown - // both dispatch through them). - ...streamVtStampLines(this), - // Node reads NODE_EXTRA_CA_CERTS and the referenced file during - // process initialization. Native fetch performs the same idempotent - // install from scr_fetch_install. - ...(snapshotsTlsCa ? [` scr_tls_ca_install();`] : []), - ` scr_lib_init(argc, argv);`, - // Fetch-referencing programs register the native fetch bridge before any - // island entry (the engine's lazy boot consults it): the ONLY - // reference to scr_fetch.c, so fetch-free builds never compile or - // link it (native-toolchain.ts gates on the same predicate). moduleUsesFetch is - // true only for fetch-referencing embedded graphs and for USER-code - // fetch (the island-backed ambient's globalGet) — both boot the - // engine before the global is read. - ...(moduleUsesFetch(this.mod) ? [` scr_fetch_install();`] : []), - // Embedded graphs that import node:zlib register the island's zlib - // bridge before any island entry — the ONLY reference to - // scr_zlib_island.c (native-toolchain.ts compiles it on the same predicate), so - // zlib-free dynamic builds keep the island's clear refusal. - ...(moduleEmbedsBuiltin(this.mod, "node:zlib") ? [` scr_zlib_island_install();`] : []), - // Embedded graphs that import node:http/https register the island's - // http client bridge (scr_net_island.c — native-toolchain.ts compiles it and the - // socket units on the same predicate; native-fetch builds also - // register it from scr_fetch_install, idempotently). - ...(moduleEmbedsBuiltin(this.mod, "node:http") || moduleEmbedsBuiltin(this.mod, "node:https") - ? [` scr_net_island_install();`] - : []), - // Event-surface programs (signal/exit listeners, stdin events) fill - // the loop's nullable event hooks before %main — the events unit - // (scr_events.c) links only when this line is emitted (native-toolchain.ts gates - // on the same predicate, like fetch). - ...(moduleUsesProcessEvents(this.mod) ? [` scr_events_install();`] : []), - ...(moduleUsesChildProcess(this.mod) ? [` scr_child_dyn_install();`] : []), - // Net-surface programs fill the loop's net hooks before %main — the - // net unit (scr_net.c) links only when this line is emitted (native-toolchain.ts - // gates on the same predicate, like events). The dyn-install twin - // stamps the netSocket handle-dispatch ops into the dyn core so - // sockets can cross the checked-dynamic boundary (SCR_DYN_HANDLE). - ...(moduleUsesNet(this.mod) ? [` scr_net_install();`, ` scr_net_dyn_install();`] : []), - // Http-surface programs additionally stamp the httpReq/httpRes - // handle-dispatch ops (scr_http.c links exactly when this line is - // emitted — native-toolchain.ts's http gate). - ...(moduleUsesHttpServer(this.mod) ? [` scr_http_dyn_install();`] : []), - // http2-surface programs stamp the h2 session/stream handle-dispatch - // ops (scr_http2.c links exactly when this line is emitted — native-toolchain.ts's - // http2 gate). - ...(moduleUsesHttp2(this.mod) ? [` scr_http2_dyn_install();`] : []), - // Stream-surface programs fill the loop's stream hook (the deferred - // next-tick emissions) before %main — scr_stream.c links only when - // this line is emitted (native-toolchain.ts gates on the same predicate). - ...(moduleUsesStream(this.mod) ? [` scr_stream_install();`] : []), - // Dgram/dns-surface programs fill the loop's dgram hooks the same - // way — scr_dgram.c links only when this line is emitted. - ...(moduleUsesDgram(this.mod) ? [` scr_dgram_install();`] : []), - // fs.watch programs fill the loop's watch hooks the same way — - // scr_watch.c links only when this line is emitted. - ...(moduleUsesFsWatch(this.mod) ? [` scr_watch_install();`] : []), - ...(this.ffiHasForeignCallback ? [` scr_ffi_install();`] : []), - // The embedded npm tables must be registered before %main: the %init - // functions it calls import from them. Static data only — the engine - // still boots lazily, on the first island entry. Compressed module - // text (island.ts stores big sources as raw DEFLATE) needs the - // inflater installed first — scr_zlib.c joins the link on the same - // moduleEmbedsCompressedNpm predicate (index.ts's zlib switch), so - // compression-free dynamic builds keep their exact link line. - ...(embedded && embedded.modules.length > 0 - ? [ - ...(moduleEmbedsCompressedNpm(this.mod) - ? [` scr_island_set_inflate(scr_zlib_inflate_exact);`] - : []), - ` scr_island_modules(sc_npm_modules, ${embedded.modules.length}, ` + - `${embedded.edges.length > 0 ? "sc_npm_edges" : "NULL"}, ${embedded.edges.length});`, - ] - : []), - ...(asyncEntry - ? [` ScrPromise *sc_top = ${mangleAsyncSpawn(this.mod.entry)}();`] - : [` ${mangleFunction(this.mod.entry)}();`]), - // Uncaught exception from top-level code: Node exits 1. - ...(this.mayThrow.has(this.mod.entry) && !asyncEntry ? uncaught(" ", false, true) : []), - // The event loop runs to exhaustion (microtasks before timers). A - // throw escaping a timer callback and unhandled promise rejections - // both exit 1, like Node. - ...(hasAsync || hasGenerators || this.usesTimers || usesIsland || this.ffiHasForeignCallback - ? [ - ` bool sc_loop_rejection = scr_loop_run(${asyncEntry ? "sc_top" : "NULL"});`, - ...uncaught(" ", asyncEntry), - ` if (sc_loop_rejection) {`, - ` scr_discard_unhandled_rejections();`, - ...(asyncEntry ? [` scr_promise_release(sc_top);`] : []), - ` ${exitCleanup}return scr_exit_code_hint_get();`, - ` }`, - ...(asyncEntry - ? [ - ` int sc_top_status = scr_promise_finish_top_level(sc_top);`, - ` if (sc_top_status == 1) {`, - // Earlier-checkpoint rejections were decided inside the - // loop. The fatal module verdict suppresses unrelated - // rejections from THIS checkpoint, exactly Node's order. - ` scr_discard_unhandled_rejections();`, - ` scr_promise_rethrow_top_level(sc_top);`, - ` scr_promise_release(sc_top);`, - ` scr_exc_print_uncaught();`, - ` ${exitCleanup}return scr_exit_code_hint_get();`, - ` }`, - ` scr_promise_release(sc_top);`, - ] - : []), - ` if (scr_report_unhandled_rejections()) {`, - ` ${exitCleanup}return scr_exit_code_hint_get();`, - ` }`, - ...(asyncEntry - ? [ - ` if (sc_top_status == 13) {`, - ` int sc_exit_status = ${programExitCode};`, - ` if (sc_exit_status == 0) sc_exit_status = sc_top_status;`, - ...(programExitUsesIsland && runExitListeners !== "" - ? [` size_t sc_exit_code_version = scr_island_exit_code_version();`] - : []), - // finish_top_level initially notes 13; replace that hint - // before exit listeners run when a higher-priority - // verdict has already selected the process status. - ` scr_exit_code_note(sc_exit_status);`, - ...(runExitListeners !== "" ? [` ${runExitListeners.trim()}`] : []), - ...(programExitUsesIsland && runExitListeners !== "" - ? [ - ` if (scr_island_exit_code_version() != sc_exit_code_version) {`, - ` sc_exit_status = scr_island_exit_code();`, - ` scr_exit_code_note(sc_exit_status);`, - ` }`, - ] - : []), - ...(needsRelease ? [` sc_release_globals();`] : []), - ` return sc_exit_status;`, - ` }`, - ] - : []), - ] - : []), - releaseGlobals, - // node:test programs exit through the runner's verdict (Node's - // contract: 1 when any non-todo test failed). The loop-run above is - // guaranteed for these programs — every registration libCall sets - // usesTimers, so the runner fiber always drains before this line. - // Island programs exit with process.exitCode when the embedded - // graph set it (Node's implicit exit status: set it, return - // normally, exit with it) — 0 when never set. - ` return ${programExitCode};`, - `}`, - ``, - ); - return out; - } - - /* ── library mode ───────────────────────────────────────────────────── - * The program TU's ONLY external-linkage definitions: the export-map - * wrappers plus the mode-provided init / sink-registration / reset / - * collect entries, all delegating their runtime halves to scr_library.c so - * both backends' emitted bodies are trivially identical. The init entry - * IS module-graph evaluation: full deterministic reset (program globals - * released and zeroed — run-once guards included — then the runtime's - * session reset), the error-vt interval stamps verbatim from today's - * main, then %main, then the escaped-exception check. */ - emitLibEntries(out: string[], globals: IrGlobal[]): void { - const lib = this.mod.lib!; - const autoReset = lib.resultResetSymbol === null; - out.push(``, `/* ── library-mode entries (profile: ${lib.profileName}) ── */`, ``); - // Session reset of PROGRAM state: release every refcounted global and - // zero everything (the run-once module guards included), putting the - // program back at the not-yet-evaluated state. - out.push(`static void sc_lib_release_globals(void) {`); - for (const g of globals) { - const name = mangleGlobal(g.id); - if (isRefCounted(g.type)) { - out.push(` ${releaseCallC(g.type, name)}; ${name} = NULL; /* ${g.name} */`); - } else if (g.type.kind === "bool") { - out.push(` ${name} = false; /* ${g.name} */`); - } else { - out.push(` ${name} = 0; /* ${g.name} */`); - } - } - if (globals.length === 0) out.push(` /* no globals */`); - out.push(`}`, ``); - - // 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. NULL keeps the funnel's default for that cell; - // the empty table still defines the symbols the funnel links against. - if (lib.trapOverlays.length === 0) { - out.push(`const char *const scr_library_trap_overlays[] = { NULL };`); - } else { - const cells = lib.trapOverlays.flatMap((o) => [ - cStringLiteral(Buffer.from(o.code, "utf8")), - o.teaching !== undefined ? cStringLiteral(Buffer.from(o.teaching, "utf8")) : "NULL", - o.remediation !== undefined ? cStringLiteral(Buffer.from(o.remediation, "utf8")) : "NULL", - ]); - out.push(`const char *const scr_library_trap_overlays[] = { ${cells.join(", ")} };`); - } - out.push(`const size_t scr_library_trap_overlays_len = ${lib.trapOverlays.length};`, ``); - - out.push( - `void ${lib.initSymbol}(void) {`, - ` scr_library_entry(true, "${lib.initSymbol}"); /* init always resets the result arena */`, - ` sc_lib_release_globals();`, - ` scr_library_reset();`, - ...this.errorVtStampLines(), - ...emitterVtStampLines(this), - ` ${mangleFunction(this.mod.entry)}();`, - ` scr_library_check_exc();`, - `}`, - ``, - `void ${lib.sinkRegisterSymbol}(void (*fn)(void *ctx, const uint8_t *msg, size_t msg_len, uint64_t address), void *ctx) {`, - ` scr_library_callback_entry_guard("${lib.sinkRegisterSymbol}");`, - ` scr_library_set_sink(fn, ctx);`, - `}`, - ``, - ); - if (lib.callbacks !== undefined && lib.callbacks.length > 0) { - // Host-callback registration: a pure store dispatch (the sink - // registration's rule — no entry prologue, no poison guard, legal - // before init) except the first operation rejects callback-time - // re-entry (SC4026). The channel name selects the slot; an unknown or - // NULL name is a defined -1, never a store. Latest registration - // wins; a NULL fn clears the channel. - out.push( - `int32_t ${lib.callbackRegisterSymbol}(const char *name, void (*fn)(void), void *ctx) {`, - ` scr_library_callback_entry_guard("${lib.callbackRegisterSymbol}");`, - ` if (name == NULL) return -1;`, - ); - for (const cb of lib.callbacks) { - out.push( - ` if (strcmp(name, ${cStringLiteral(Buffer.from(cb.name, "utf8"))}) == 0) {`, - ` scr_library_cb_set(${cb.slot}, fn, ctx); /* channel '${cb.name}' */`, - ` return 0;`, - ` }`, - ); - } - out.push(` return -1;`, `}`, ``); - } - if (lib.identity !== undefined && this.options.emitLibraryIdentity !== false) { - // Profile-declared identity getters (the ask-2 sidecar's boot-time - // pairing fence): pure data returns with NO entry prologue — exempt - // from the poisoned guard and every runtime touch (ratified), so a - // host can read them before init and after a trap. - out.push(...emitLibraryIdentityLines("c", lib.identity)); - } - if (lib.resultResetSymbol !== null) { - out.push( - `void ${lib.resultResetSymbol}(void) {`, - ` scr_library_entry(false, "${lib.resultResetSymbol}");`, - ` scr_library_arena_reset();`, - `}`, - ``, - ); - } - if (lib.collectSymbol !== null) { - out.push( - `void ${lib.collectSymbol}(void) {`, - ` scr_library_entry(false, "${lib.collectSymbol}");`, - ` scr_library_collect(); /* arena reset + a full cycle collection */`, - `}`, - ``, - ); - } - for (const e of lib.exports) { - const params: string[] = []; - const args: string[] = []; - e.params.forEach((cls, i) => { - switch (cls) { - case "f64": - params.push(`double a${i}`); - args.push(`a${i}`); - break; - case "bool": - params.push(`uint8_t a${i}`); - args.push(`(a${i} != 0)`); - break; - case "u8": - params.push(`uint8_t a${i}`); - args.push(`(double)a${i}`); - break; - case "u32": - params.push(`uint32_t a${i}`); - args.push(`(double)a${i}`); - break; - case "i32": - params.push(`int32_t a${i}`); - args.push(`(double)a${i}`); - break; - case "i64": - // The inbound declared-integer edge (ask 4): the helper - // converts exactly or delivers the host-contract trap — a - // value past ±(2^53−1) cannot ride f64 without silent - // rounding, which is a coercion the author never wrote. - params.push(`int64_t a${i}`); - args.push(`scr_library_i64_in(a${i}, ${cStringLiteral(Buffer.from(e.inboundIntTrap!, "utf8"))})`); - break; - case "u64": - params.push(`uint64_t a${i}`); - args.push(`scr_library_u64_in(a${i}, ${cStringLiteral(Buffer.from(e.inboundIntTrap!, "utf8"))})`); - break; - case "string": - params.push(`const uint8_t *a${i}_ptr`, `size_t a${i}_len`); - args.push(`scr_library_str_in(a${i}_ptr, a${i}_len)`); - break; - case "bytes": - params.push(`const uint8_t *a${i}_ptr`, `size_t a${i}_len`); - // The helper's trap message is the compiler-assembled - // structured trap-teaching form (0x01 text 0x1F SC4012 0x1F - // symbol [0x1F remediation]) — assembled once at export - // resolution, identical across both backends. - args.push(`scr_library_bytes_in(a${i}_ptr, a${i}_len, ${cStringLiteral(Buffer.from(e.inboundBytesTrap!, "utf8"))})`); - break; - } - }); - if (e.returns === "string" || e.returns === "bytes") { - params.push(`const uint8_t **out`, `size_t *out_len`); - } - const retType = - e.returns === "f64" ? "double" - : e.returns === "bool" ? "uint8_t" - : e.returns === "i64" ? "int64_t" - : e.returns === "u64" ? "uint64_t" - : "void"; - const call = `${mangleFunction(e.fnName)}(${args.join(", ")})`; - out.push(`${retType} ${e.symbol}(${params.length > 0 ? params.join(", ") : "void"}) {`); - // The prologue records this entry's symbol in the funnel's - // current-entry slot: a detected trap anywhere below names the - // entry the host called (structured trap-teaching field 2). - out.push(` scr_library_entry(${autoReset ? "true" : "false"}, "${e.symbol}");`); - switch (e.returns) { - case "void": - out.push(` ${call};`, ` scr_library_check_exc();`); - break; - case "f64": - out.push(` double sc_r = ${call};`, ` scr_library_check_exc();`, ` return sc_r;`); - break; - case "i64": - case "u64": - // The outbound declared-integer edge (ask 4): every value - // reaching this return was PROVEN whole and inside the class's - // range at compile time, so the cast is exact by construction - // (and the unwind path's 0.0 converts cleanly). - out.push( - ` double sc_r = ${call};`, - ` scr_library_check_exc();`, - ` return (${retType})sc_r;`, - ); - break; - case "bool": - out.push(` bool sc_r = ${call};`, ` scr_library_check_exc();`, ` return (uint8_t)(sc_r ? 1 : 0);`); - break; - case "string": - out.push(` ScrStr *sc_r = ${call};`, ` scr_library_check_exc();`, ` scr_library_str_out(sc_r, out, out_len);`); - break; - case "bytes": - out.push(` ScrBytes *sc_r = ${call};`, ` scr_library_check_exc();`, ` scr_library_bytes_out(sc_r, out, out_len);`); - break; - } - out.push(`}`, ``); - } - } - - emitAsyncScaffolding(out: string[]): void { - return emitAsyncScaffolding(this, out); - } - - emitStructDefs(out: string[]): void { - return emitStructDefs(this, out); - } - - /* ── vtables (class hierarchies) ────────────────────────────────────── - * One vtable struct type per hierarchy (named after the root): a ScrVt - * head — the class's preorder interval for instanceof and its DIRECT - * release for dynamic teardown — plus one member per virtual slot. Only - * methods actually overridden somewhere have slots; the slot's signature - * is the root-most declaring class's, and overriding implementations sit - * behind reinterpreting adapters (prefix layout makes the `this` cast - * sound). Each class gets one static const vtable instance stamped into - * every object it allocates. */ - - vtEntriesFor(meta: ClassMeta): { slot: VtSlot; impl: ClassMeta | null }[] { - return vtEntriesFor(this, meta); - } - - vtSlotParams(slot: VtSlot, named: boolean): string[] { - return vtSlotParams(this, slot, named); - } - - emitVtableDecls(out: string[], hierarchyClasses: ClassMeta[]): void { - return emitVtableDecls(this, out, hierarchyClasses); - } - - emitVtableInstances(out: string[], hierarchyClasses: ClassMeta[]): void { - return emitVtableInstances(this, out, hierarchyClasses); - } - - emitVtAdapterDefs(out: string[]): void { - return emitVtAdapterDefs(this, out); - } - - emitHierarchyClassHelpers(out: string[], - meta: ClassMeta, - s: StructShape,): void { - return emitHierarchyClassHelpers(this, out, meta, s); - } - - /* ── type-directed JSON walkers (jsonStringify / dynCheck) ──────────── - * No dyn is built for stringify and no tags are consulted: the STATIC IR - * type drives everything, one emitted helper per typeKey (interned, like - * the array-HOF desugars). dynCheck helpers walk the runtime dyn (ScrDyn) - * against the target type: match predicates (sc_dm_*) answer "does this - * dyn fit?" without throwing (union arms are tried in canonical order — - * first FULL match wins), and builders (sc_dc_*) construct the typed - * value (+1) or throw a path-annotated TypeError through the exception - * cell. Recursion terminates because recursive shapes/unions are rejected - * by the frontend. */ - - islandAdapter(arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string { - return islandAdapter(this, arity, retKind); - } - - islandTypedAdapter(fn: IrType & { kind: "func" }): string { - return islandTypedAdapter(this, fn); - } - - unionTruthyHelper(unionId: string): string { - return unionTruthyHelper(this, unionId); - } - - unionEqHelper(unionId: string, sameValue: boolean): string { - return unionEqHelper(this, unionId, sameValue); - } - - unionToStrHelper(unionId: string): string { - return unionToStrHelper(this, unionId); - } - - unionJoinHelper(unionId: string): string { - return unionJoinHelper(this, unionId); - } - - jsonWriteHelper(t: IrType): string { - return jsonWriteHelper(this, t); - } - - jsonIndentHelper(): string { - return jsonIndentHelper(this); - } - - dynMatchHelper(t: IrType): string { - return dynMatchHelper(this, t); - } - - dynCheckHelper(t: IrType): string { - return dynCheckHelper(this, t); - } - - toDynHelper(t: IrType): string { - return toDynHelper(this, t); - } - - liveDynRefAdapter(t: IrType): StreamTypedRefAdapter { - return buildLiveDynRefAdapter(this, t); - } - - dynFuncBoxHelper(t: IrType & { kind: "func" }): string { - return dynFuncBoxHelper(this, t); - } - - recordKeyGetHelper(shapeId: string, t: IrType, overflowOnly = false): string { - return recordKeyGetHelper(this, shapeId, t, overflowOnly); - } - - recordKeySetHelper(shapeId: string): string { - return recordKeySetHelper(this, shapeId); - } - - dynToStrHelper(): string { - return dynToStrHelper(this); - } - - caughtToDynHelper(): string { - return caughtToDynHelper(this); - } - - /* ── cycle-collection wiring ────────────────────────────────────────── - * Containers that store a payload's RC entry points as function pointers - * (obj-kind boxes, union ref arms, promise payloads, the exception cell) - * also store the payload type's TRACE entry point — non-NULL exactly when - * the payload type carries a cycle header, which is what the container's - * own trace keys on (visit when present, release at teardown when not). - * Closures/unions/promises always carry one (runtime-provided traces); - * classes/records carry one iff their shape can participate in a cycle - * (emitted trace); strings/arrays/dyn never can. */ - - traceAdapterC(t: IrType): string | null { - return traceAdapterC(this, t); - } - - traceArgC(t: IrType): string { - return traceArgC(this, t); - } - - boxNewC(t: IrType): string { - return boxNewC(this, t); - } - - arrNewC(elem: IrType, capExpr: string | number): string { - return arrNewC(this, elem, capExpr); - } - - /* ── plumbing ─────────────────────────────────────────────────────── */ - - integerLoopIndex(expr: IrExpr): string | null { - return expr.kind === "varRef" ? this.integerLoopBindings.get(expr.localId) ?? null : null; - } - - bytesElementHelper(op: "get" | "set", elem: IrBytesElem, integerIndex = false): string { - const mode = integerIndex ? "u64" : "f64"; - this.bytesElementHelpers.add(`${op}:${elem}:${mode}`); - return `sc_bytes_${op}_${elem}${integerIndex ? "_u64" : ""}`; - } - - private emitBytesElementHelpers(): string[] { - if (this.bytesElementHelpers.size === 0) return []; - const hasF64 = [...this.bytesElementHelpers].some((key) => key.endsWith(":f64")); - const hasU64 = [...this.bytesElementHelpers].some((key) => key.endsWith(":u64")); - const out = [`/* Typed-array hot paths specialized from the IR element kind. */`]; - if (hasF64) { - out.push( - `static inline size_t sc_bytes_index_checked(const ScrBytes *b, double i) {`, - ` if (!(i >= 0.0 && i < (double)b->len)) { (void)scr_bytes_get(b, i); return 0; }`, - ` size_t idx = (size_t)i;`, - ` if ((double)idx != i) { (void)scr_bytes_get(b, i); return 0; }`, - ` return idx;`, - `}`, - ); - } - if (hasU64) { - out.push( - `static inline size_t sc_bytes_index_u64_checked(const ScrBytes *b, uint64_t i) {`, - ` if (i >= b->len) { (void)scr_bytes_get(b, (double)i); return 0; }`, - ` return (size_t)i;`, - `}`, - ); - } - if ([...this.bytesElementHelpers].some((key) => key.startsWith("set:") && !key.startsWith("set:f32:") && !key.startsWith("set:f64:"))) { - out.push( - `static inline uint32_t sc_bytes_coerce_u32(double v) {`, - ` if (v >= -9007199254740992.0 && v <= 9007199254740992.0) return (uint32_t)(int64_t)v;`, - ` if (v != v || isinf(v)) return 0;`, - ` double t = fmod(trunc(v), 4294967296.0);`, - ` if (t < 0) t += 4294967296.0;`, - ` return (uint32_t)t;`, - `}`, - ); - } - const elements: IrBytesElem[] = ["u8", "u8c", "i8", "u16", "i16", "u32", "i32", "f32", "f64"]; - const modes: ("f64" | "u64")[] = ["f64", "u64"]; - for (const elem of elements) { - for (const mode of modes) { - const suffix = mode === "u64" ? "_u64" : ""; - const indexType = mode === "u64" ? "uint64_t" : "double"; - const checked = mode === "u64" ? "sc_bytes_index_u64_checked" : "sc_bytes_index_checked"; - if (this.bytesElementHelpers.has(`get:${elem}:${mode}`)) { - if (elem === "u8") { - out.push( - `static inline double sc_bytes_get_u8${suffix}(const ScrBytes *b, ${indexType} i) {`, - ` return (double)b->data[${checked}(b, i)];`, - `}`, - ); - } else { - const size = BYTES_ELEMENT_SIZE[elem]; - const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `${elem.startsWith("i") ? "int" : "uint"}${size * 8}_t`; - out.push( - `static inline double sc_bytes_get_${elem}${suffix}(const ScrBytes *b, ${indexType} i) {`, - ` ${valueType} v;`, - ` memcpy(&v, b->data + ${checked}(b, i) * ${size}, ${size});`, - ` return (double)v;`, - `}`, - ); - } - } - if (this.bytesElementHelpers.has(`set:${elem}:${mode}`)) { - if (elem === "u8") { - out.push( - `static inline void sc_bytes_set_u8${suffix}(ScrBytes *b, ${indexType} i, double v) {`, - ` b->data[${checked}(b, i)] = (uint8_t)sc_bytes_coerce_u32(v);`, - `}`, - ); - } else { - const size = BYTES_ELEMENT_SIZE[elem]; - const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `uint${size * 8}_t`; - const init = elem === "f32" ? `(float)v` : elem === "f64" ? "v" : elem === "u8c" ? `(uint8_t)scr_bytes_to_u8_clamp(v)` : `(${valueType})sc_bytes_coerce_u32(v)`; - out.push( - `static inline void sc_bytes_set_${elem}${suffix}(ScrBytes *b, ${indexType} i, double v) {`, - ` size_t idx = ${checked}(b, i);`, - ` ${valueType} stored = ${init};`, - ` memcpy(b->data + idx * ${size}, &stored, ${size});`, - `}`, - ); - } - } - } - } - out.push(""); - return out; - } - - line(text: string): void { - const pos = this.sourceLoc === null ? null : this.debug?.position(this.sourceLoc); - if (pos !== null && pos !== undefined && text.trim() !== "") { - // Repeat the directive for every generated line: one TS statement can - // expand into many native instructions, all belonging to that line. - this.lines.push(`#line ${pos.line} ${JSON.stringify(pos.file)}`); - } - this.lines.push(" ".repeat(this.indent) + text); - } - - endSourceFunction(): void { - this.sourceLoc = null; - if (this.debug !== null) this.lines.push('#line 1 ""'); - } - - srcComment(loc: SrcLoc): string { - if (!this.lineStarts) return ""; - let lo = 0, hi = this.lineStarts.length - 1; - while (lo < hi) { - const mid = (lo + hi + 1) >> 1; - if (this.lineStarts[mid]! <= loc.start) lo = mid; - else hi = mid - 1; - } - return ` /* ${this.mod.sourceFile}:${lo + 1} */`; - } - - newTemp(type: IrType, init: string): Temp { - const name = `sc_t${this.tempCounter++}`; - this.line(`${cDecl(type, name)} = ${init};`); - if (isRefCounted(type)) this.currentFrame().push({ name, type }); - return { name, type }; - } - - /** A borrowed compiler temp. Used only when a surrounding operation has - * proved that evaluation cannot overwrite the owning binding before the - * temp's last use. Unlike newTemp, this does not join the release frame. */ - newBorrowedTemp(type: IrType, init: string): Temp { - const name = `sc_t${this.tempCounter++}`; - this.line(`${cDecl(type, name)} = ${init};`); - return { name, type }; - } - - /** newTemp for a MAY-THROW runtime call: the result joins its frame - * BEFORE the standard pending check, so an unwind releases the dummy - * (NULL for refcounted kinds) harmlessly and the value is only read past - * the check. The shared shape of every fallible boundary call — jsOp, - * jsExit, composite jsMarshal, dynCheck, await reads. */ - fallibleTemp(type: IrType, call: string): Temp { - const t = this.newTemp(type, call); - this.emitPendingCheck(); - return t; - } - - currentFrame(): ScopeEntry[] { - const frame = this.frames[this.frames.length - 1]; - if (!frame) throw new InternalCompilerError("emitter bug: no active statement frame"); - return frame; - } - - /** Strike a refcounted temp from its frame: ownership is being moved. */ - moveTemp(t: Temp): void { - if (!isRefCounted(t.type)) return; - for (let i = this.frames.length - 1; i >= 0; i--) { - const idx = this.frames[i]!.findIndex((e) => e.name === t.name); - if (idx >= 0) { - this.frames[i]!.splice(idx, 1); - return; - } - } - throw new InternalCompilerError(`emitter bug: moved temp ${t.name} not found in any frame`); - } - - /** The release call for one owned refcounted value. */ - releaseValue(name: string, type: IrType): void { - this.line(`${releaseCallC(type, name)};`); - } - - releaseFrame(frame: ScopeEntry[]): void { - for (const t of frame) { - if (t.boxed) this.line(`scr_box_release(${t.name});`); - else this.releaseValue(t.name, t.type); - } - } - - /** THE release-on-jump path (break/continue/return): pending statement - * frames and entered scopes down to (and excluding nothing above) the - * given depths, innermost first — everything whose normal fall-through - * releases the jump bypasses. The jump target's own frame/scope stay - * live: a loop's releases after the loop and a switch's after its end - * label are still on the jump's path (return passes 0/0 — it leaves the - * whole function). */ - 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.releaseFrame(this.scopes[i]!); - } - - /** Run the finally regions a break/continue exits, then release to its - * resolved target. Every body sees compile-time frame/scope/finally/try - * stacks truncated to the region boundary: a throw propagates outside - * the completing try, and a jump inside the finally replaces the pending - * jump without re-entering the same cleanup. */ - emitFinallysForJump(finallyDepth: number, frameDepth: number, scopeDepth: number): void { - if (this.finallyStack.length <= finallyDepth) { - this.releaseForJump(frameDepth, scopeDepth); - return; - } - const savedFrames = this.frames; - const savedScopes = this.scopes; - const savedFinally = this.finallyStack; - const savedTry = this.tryStack; - for (let i = savedFinally.length - 1; i >= finallyDepth; i--) { - const fin = savedFinally[i]!; - this.releaseForJump(fin.frameDepth, fin.scopeDepth); - this.frames = this.frames.slice(0, fin.frameDepth); - this.scopes = this.scopes.slice(0, fin.scopeDepth); - this.finallyStack = savedFinally.slice(0, i); - this.tryStack = savedTry.slice(0, fin.tryDepth); - this.emitBlock(fin.body); - } - this.releaseForJump(frameDepth, scopeDepth); - this.frames = savedFrames; - this.scopes = savedScopes; - this.finallyStack = savedFinally; - this.tryStack = savedTry; - } - - /** THE unwind path at a point where an exception is pending: release - * everything between here and the innermost try handler — or the whole - * function — via releaseForJump, then jump 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 - * `if (scr_exc_pending())`; a `throw` unwinds unconditionally. */ - emitUnwind(): void { - const target = this.tryStack[this.tryStack.length - 1]; - if (target) { - this.releaseForJump(target.frameDepth, target.scopeDepth); - target.used = true; - this.line(`goto ${target.label};`); - return; - } - this.releaseForJump(0, 0); - const t = this.currentReturnType; - if (t.kind === "void") this.line(`return;`); - else if (t.kind === "f64" || t.kind === "date") this.line(`return 0;`); - else if (t.kind === "bool") this.line(`return false;`); - else this.line(`return NULL;`); - } - - errorVtStampLines(): string[] { - return errorVtStampLines(this); - } - - /** The C symbol a direct call or closure enters a function through: - * 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). */ - callTargetC(fnName: string): string { - const fn = this.fnByName.get(fnName); - if (fn?.generator !== undefined) return mangleGenSpawn(fnName); - if (fn?.async === true) return mangleAsyncSpawn(fnName); - return mangleFunction(fnName); - } - - /** The emitter contract for exceptions: after EVERY call that can throw - * (per the may-throw analysis), test the pending flag and unwind. */ - emitPendingCheck(): void { - this.line(`if (scr_exc_pending()) {`); - this.indent++; - this.emitUnwind(); - this.indent--; - this.line(`}`); - } - - internLiteral(text: string): string { - let sym = this.literals.get(text); - if (!sym) { - sym = `sc_lit_${this.literals.size}`; - this.literals.set(text, sym); - } - return sym; - } - - /** The class object's static symbol (classes as values), registering it - * for assembly on first use and interning the .name literal while the - * literal table is still open (bodies emit before the file assembles — - * the regex-literal discipline). */ - classObjSym(className: string): string { - let sym = this.classObjs.get(className); - if (!sym) { - const meta = this.classMeta.get(className); - if (!meta) throw new InternalCompilerError(`emitter bug: classRef to unknown class ${className}`); - this.internLiteral(meta.def.jsName ?? ""); - sym = mangleClassObj(className); - this.classObjs.set(className, sym); - } - return sym; - } - - /** The interned immortal instance for a UNIT arm of a union — asserts the - * arm really is payload-less (undefined/null). */ - internUnitInstance(unionId: string, tag: number): string { - const arm = this.unionsById.get(unionId)?.arms[tag]; - if (!arm || !isUnitType(arm)) { - throw new InternalCompilerError(`emitter bug: unit instance for non-unit arm ${tag} of ${unionId}`); - } - const key = `${unionId}:${tag}`; - let sym = this.unitInstances.get(key); - if (!sym) { - sym = `sc_unit_${this.unitInstances.size}`; - this.unitInstances.set(key, sym); - } - return sym; - } - - /** The C expression for that instance — the one pointer every unit-arm - * value of this (union, tag) is: rc == SIZE_MAX, so RC entry points and - * the collector both skip it and no retain is ever owed. */ - unitInstanceRef(unionId: string, tag: number): string { - return `(ScrUnion *)&${this.internUnitInstance(unionId, tag)}`; - } - - /** The class-newFn initialization line for a field whose type ADMITS - * undefined — such fields start as JS's `undefined`, never the calloc - * NULL: tsc's strictPropertyInitialization accepts them 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 payload pointer would be a segfault (union - * fields), or an invalid dyn/jsval cell. Native checked-dynamic fields - * get the immortal dyn undefined. Undefined-armed unions get - * the interned immortal unit instance (free; releases skip it); jsval - * (`any`) fields get an engine undefined cell — such classes exist only - * in --dynamic builds, and the field's release balances it. Empty for - * every type that cannot hold undefined (tsc's SPI guards those) and for - * record shapes' construction paths, which write every field. */ - undefFieldInitLineC(name: string, t: IrType): string[] { - // Error.cause uses NULL for absence; only the options constructor may - // install a present value (including undefined) in this runtime slot. - if (name === "%cause") return []; - if (t.kind === "dyn") { - return [` o->${mangleField(name)} = scr_dyn_undefined(); /* ${cCommentText(name)} starts undefined */`]; - } - if (t.kind === "jsval") { - return [` o->${mangleField(name)} = scr_jsval_undefined(); /* ${cCommentText(name)} starts undefined */`]; - } - const tag = undefinedArmTag(t, this.unionsById); - if (tag < 0 || t.kind !== "union") return []; - return [` o->${mangleField(name)} = ${this.unitInstanceRef(t.unionId, tag)}; /* ${cCommentText(name)} starts undefined */`]; - } - - /* ── functions ────────────────────────────────────────────────────── */ - - ffiCallbackAdapter(binding: string, id: string): FfiCallbackAdapter { - const adapter = this.ffiCallbackAdapters.get(`${binding}:${id}`); - if (!adapter) throw new InternalCompilerError(`emitter bug: no callback adapter for ${binding}:${id}`); - return adapter; - } - - /** C-callable trampolines for format-2/3/4 callbacks. The external callback - * ABI is scalar C, format-3 copy-in string/byte slots, plus an optional - * exact-position context pointer; the internal side is scriptc's - * (ScrClosure *env, params...) convention. A raw callback borrows its - * closure through a distinct TLS slot for the dynamic extent of the outer - * call, or a process-global slot for retained replace semantics. */ - emitFfiCallbackDefs(out: string[]): void { - if (this.ffiCallbackAdapters.size === 0) return; - for (const adapter of this.ffiCallbackAdapters.values()) { - const cb = adapter.callback; - if (adapter.tls !== null) { - out.push(`static _Thread_local ScrClosure *${adapter.tls};`); - } - if (adapter.global !== null) { - out.push(`static _Thread_local ScrClosure *${adapter.global};`); - } - if (adapter.table !== null) { - out.push(`static ScrFfiTable ${adapter.table};`); - } - const nativeParams = ffiCallbackNativeParamsC(cb, true); - const ret = ffiNativeTypeC(cb.returns); - const contextParam = cb.params.findIndex(isFfiContextParam); - if (cb.invoke === "foreign") { - if (adapter.table === null || contextParam < 0 || cb.returns !== "void") { - throw new InternalCompilerError("emitter bug: invalid foreign FFI callback descriptor"); - } - const dispatch = `${adapter.symbol}_dispatch`; - const ft = ffiCallbackType(cb); - const scriptArgs = cb.params.flatMap((param, i): string[] => { - if (isFfiContextParam(param)) return []; - switch (param) { - case "i64": - case "u64": - case "pointer": - throw new InternalCompilerError("64-bit and pointer foreign callbacks require native queue support"); - case "f64": - case "f32": - case "i8": - case "u16": - case "i16": - return [`scr_ffi_call_get_f64(sc_call, ${i})`]; - case "bool": - return [`scr_ffi_call_get_bool(sc_call, ${i})`]; - case "u8": - return [`scr_ffi_call_get_u8(sc_call, ${i})`]; - case "u32": - return [`scr_ffi_call_get_u32(sc_call, ${i})`]; - case "i32": - return [`scr_ffi_call_get_i32(sc_call, ${i})`]; - case "cstring": - case "string": - return [`scr_str_from_utf8_lossy(scr_ffi_call_get_data(sc_call, ${i}), scr_ffi_call_get_len(sc_call, ${i}))`]; - case "bytes": - return [`scr_bytes_from_data(scr_ffi_call_get_data(sc_call, ${i}), scr_ffi_call_get_len(sc_call, ${i}))`]; - } - }); - out.push( - `static void ${dispatch}(ScrClosure *sc_cb, ScrFfiCall *sc_call) {`, - ` (${cFnPtrCast(ft)}sc_cb->fn)(sc_cb${scriptArgs.length ? `, ${scriptArgs.join(", ")}` : ""});`, - `}`, - `static void ${adapter.symbol}(${nativeParams.join(", ")}) {`, - ` ScrClosure *sc_cb = (ScrClosure *)sc_ctx;`, - ` ScrFfiCall *sc_call = scr_ffi_call_new(&${adapter.table}, sc_cb, &${dispatch}, ${cb.params.length});`, - ); - cb.params.forEach((param, i) => { - if (isFfiContextParam(param)) return; - switch (param) { - case "f64": - case "f32": - case "i8": - case "u16": - case "i16": - out.push(` scr_ffi_call_set_f64(sc_call, ${i}, sc_a${i});`); - break; - case "bool": - out.push(` scr_ffi_call_set_bool(sc_call, ${i}, sc_a${i});`); - break; - case "u8": - out.push(` scr_ffi_call_set_u8(sc_call, ${i}, sc_a${i});`); - break; - case "u32": - out.push(` scr_ffi_call_set_u32(sc_call, ${i}, sc_a${i});`); - break; - case "i32": - out.push(` scr_ffi_call_set_i32(sc_call, ${i}, sc_a${i});`); - break; - case "cstring": - out.push(` scr_ffi_call_copy_cstring(sc_call, ${i}, sc_a${i});`); - break; - case "string": - case "bytes": - out.push(` scr_ffi_call_copy_${param}(sc_call, ${i}, sc_a${i}, sc_a${i}_len);`); - break; - } - }); - out.push(` scr_ffi_post(sc_call);`, `}`, ``); - continue; - } - out.push( - `static ${ret} ${adapter.symbol}(${nativeParams.length > 0 ? nativeParams.join(", ") : "void"}) {`, - ` ScrClosure *sc_cb = ${contextParam >= 0 ? `(ScrClosure *)sc_ctx` : adapter.tls ?? adapter.global};`, - ` if (sc_cb == NULL) scr_trap("scriptc: native callback invoked outside its ${adapter.callback.lifetime === "call" ? "call-scoped" : "retained"} lifetime\\n");`, - ); - const dummy = ffiCallbackDummyC(cb); - out.push(` if (scr_exc_pending()) ${cb.returns === "void" ? "return;" : `return ${dummy};`}`); - // Reject every invalid native pointer before materializing any owned - // callback arguments, so the trap path cannot strand an earlier copy. - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (param === "cstring") { - out.push( - ` if (sc_a${i} == NULL) scr_trap("scriptc: native callback passed a NULL cstring\\n");`, - ); - } else if (param === "string" || param === "bytes") { - out.push( - ` if (sc_a${i} == NULL && sc_a${i}_len != 0) scr_trap("scriptc: native callback passed a NULL ${param} span with nonzero length\\n");`, - ); - } - } - const ft = ffiCallbackType(cb); - const materialized = new Map(); - for (let i = 0; i < cb.params.length; i++) { - const param = cb.params[i]!; - if (isFfiContextParam(param)) continue; - if (param === "i64" || param === "u64" || param === "pointer") { - const local = `sc_s${i}`; - out.push(` ScrBigInt *${local} = scr_bigint_from_${param}(sc_a${i});`); - materialized.set(i, local); - } else if (param === "cstring") { - const local = `sc_s${i}`; - out.push( - ` ScrStr *${local} = scr_str_from_utf8_lossy((const uint8_t *)sc_a${i}, strlen(sc_a${i}));`, - ); - materialized.set(i, local); - } else if (param === "string") { - const local = `sc_s${i}`; - out.push(` ScrStr *${local} = scr_str_from_utf8_lossy(sc_a${i}, sc_a${i}_len);`); - materialized.set(i, local); - } else if (param === "bytes") { - const local = `sc_s${i}`; - out.push(` ScrBytes *${local} = scr_bytes_from_data(sc_a${i}, sc_a${i}_len);`); - materialized.set(i, local); - } - } - const scriptArgs = cb.params.flatMap((param, i): string[] => { - if (isFfiContextParam(param)) return []; - switch (param) { - case "f64": - return [`sc_a${i}`]; - case "bool": - return [`(sc_a${i} != 0)`]; - case "u8": - case "f32": - case "i8": - case "u16": - case "i16": - case "u32": - case "i32": - return [`(double)sc_a${i}`]; - case "i64": - case "u64": - case "pointer": - case "cstring": - case "string": - case "bytes": - return [materialized.get(i)!]; - } - }); - const call = `(${cFnPtrCast(ft)}sc_cb->fn)(sc_cb${scriptArgs.length ? `, ${scriptArgs.join(", ")}` : ""})`; - if (cb.lifetime === "retained") { - // The callback may unregister or replace its own descriptor. Hold - // one invocation reference so that dropping the table's last pin - // cannot free the executing closure or its captures mid-call. - out.push(` scr_closure_retain(sc_cb);`); - } - if (cb.returns === "void") { - out.push( - ` ${call};`, - ...(cb.lifetime === "retained" ? [` scr_closure_release(sc_cb);`] : []), - ` return;`, - `}`, - ``, - ); - continue; - } - out.push(` ${cDecl(ft.ret, "sc_result")} = ${call};`); - if (cb.lifetime === "retained") out.push(` scr_closure_release(sc_cb);`); - switch (cb.returns) { - case "i64": - case "u64": - case "pointer": - out.push(` ${ffiNativeTypeC(cb.returns)} sc_out = scr_bigint_to_${cb.returns}(sc_result);`); - out.push(` scr_bigint_release(sc_result);`, ` return sc_out;`); - break; - case "f64": - out.push(` return sc_result;`); - break; - case "f32": - out.push(` return (float)sc_result;`); - break; - case "i8": - case "i16": { - const bits = cb.returns === "i8" ? 8 : 16; - out.push(` uint32_t sc_bits = (uint32_t)scr_bit_ushr(sc_result, 0.0) & ${(2 ** bits) - 1}u;`); - out.push(` return (${ffiNativeTypeC(cb.returns)})(sc_bits < ${2 ** (bits - 1)}u ? (int32_t)sc_bits : (int32_t)sc_bits - ${2 ** bits});`); - break; - } - case "u16": - out.push(` return (uint16_t)(uint32_t)scr_bit_ushr(sc_result, 0.0);`); - break; - case "bool": - out.push(` return (uint8_t)(sc_result ? 1 : 0);`); - break; - case "u8": - out.push(` return (uint8_t)(uint32_t)scr_bit_ushr(sc_result, 0.0);`); - break; - case "u32": - out.push(` return (uint32_t)scr_bit_ushr(sc_result, 0.0);`); - break; - case "i32": - out.push(` return (int32_t)scr_bit_or(sc_result, 0.0);`); - break; - } - out.push(`}`, ``); - } - } - - signature(fn: IrFunction): string { - const boxedIds = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id)); - const parts = fn.params.map((p) => - // A boxed param's sc_l_ name is its box; the raw value arrives under - // a sc_p_ name and is moved into the box in the prologue. - cDecl(p.type, boxedIds.has(p.localId) ? mangleRawParam(p.localId) : mangleLocal(p.localId)), - ); - // Lifted functions receive their closure first. - if (fn.captures !== undefined) parts.unshift("ScrClosure *sc_env"); - const params = parts.length ? parts.join(", ") : "void"; - return `static ${cType(fn.returnType)} ${mangleFunction(fn.name)}(${params})`; - } - - emitFunction(fn: IrFunction): void { - return emitFunction(this, fn); - } - - /* ── statements ───────────────────────────────────────────────────── */ - - emitBlock(stmts: IrStmt[], setup?: (scope: ScopeEntry[]) => void): void { - return emitBlock(this, stmts, setup); - } - - emitStmts(stmts: IrStmt[]): void { - return emitStmts(this, stmts); - } - - emitStmt(s: IrStmt): void { - return emitStmt(this, s); - } - - /** A fresh loop jump-target entry. `continueLabel` null means "C - * continue is correct" (while/forOf) — but a LABELED loop always - * allocates one (a labeled continue arriving from a nested loop needs a - * goto), and every labeled loop gets a lazy end label for labeled break - * (a C break only exits the innermost loop). */ - loopTarget(continueLabel: string | null, labels: string[] | undefined): (typeof this.jumpTargets)[number] & { kind: "loop" } { - return { - labels, - kind: "loop", - continueLabel: continueLabel === null && labels !== undefined ? `sc_cont_${this.labelCounter++}` : continueLabel, - usedContinue: false, - endLabel: labels !== undefined ? `sc_end_${this.labelCounter++}` : null, - usedEnd: false, - scopeDepth: this.scopes.length, - frameDepth: this.frames.length, - finallyDepth: this.finallyStack.length, - }; - } - - emitTryCatch(s: IrStmt & { kind: "tryCatch" }): void { - return emitTryCatch(this, s); - } - - emitSwitch(s: IrStmt & { kind: "switch" }): void { - return emitSwitch(this, s); - } - - mergeBrace(emitBlockFn: () => void): void { - return mergeBrace(this, emitBlockFn); - } - - emitBranchInto(target: string, expr: IrExpr): void { - return emitBranchInto(this, target, expr); - } - - emitCondition(cond: IrExpr): string { - return emitCondition(this, cond); - } - - /** C expression testing JS truthiness of a temp (falsy: 0, -0, NaN, ""). - * `x == x` rejects NaN, `x != 0` rejects both zeros; strings only need - * their length — no runtime call, no ownership change. */ - truthyC(t: Temp): string { - if (POINTER_KINDS.has(t.type.kind) && - t.type.kind !== "string" && t.type.kind !== "bigint" && t.type.kind !== "union" && - t.type.kind !== "dyn" && t.type.kind !== "jsval" && - t.type.kind !== "caught") { - // JS objects are ALWAYS truthy ([] and {} included). These are - // non-NULL pointers, so the honest constant reads as a pointer - // test (no unused-value warnings, operand still evaluated). - return `${t.name} != NULL`; - } - switch (t.type.kind) { - case "bool": - return t.name; - case "f64": - return `${t.name} == ${t.name} && ${t.name} != 0`; - case "string": - return `${t.name}->len != 0`; - case "bigint": - return `scr_bigint_truthy(${t.name})`; - case "date": - // The scalar payload may be 0 or NaN, but the source Date object - // is always truthy. - return "true"; - case "jsval": - // Island truthiness: the engine's ToBoolean (never throws, no - // ownership change) — jsval operands are legal in `logical`. - return `(scr_jsval_truthy(${t.name}) != 0)`; - case "union": - // The ARM value's ToBoolean, answered by a per-union interned - // helper (switch on tag: unit arms false, scalar/string arms by - // value, ref arms true, jsval arms ask the engine). - return `${this.unionTruthyHelper(t.type.unionId)}(${t.name})`; - case "procStream": - // A stream value is a JS object (always truthy); the scalar fd - // representation is 1 or 2, so the honest constant reads as its - // own non-zero test. - return `${t.name} != 0`; - case "dyn": - // ToBoolean over the dyn kind (scr_dyn_truthy — JS-exact for - // every kind; borrowed, never throws): `v || dflt` and condition - // descent on checked-dynamic values. - return `scr_dyn_truthy(${t.name})`; - case "undefinedT": - case "nullT": - case "caught": - throw new InternalCompilerError(`emitter bug: truthiness of ${t.type.kind}`); - case "void": - throw new InternalCompilerError("emitter bug: truthiness of void"); - default: { - const _exhaustive: never = t.type as Exclude; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - - /* ── expressions ──────────────────────────────────────────────────── */ - - emitExpr(e: IrExpr): Temp { - return emitExpr(this, e); - } - - childExitThunkFor(param: IrType): string { - return childExitThunkFor(this, param); - } - - execFileThunkFor(cbT: IrType & { kind: "func" }): string { - return execFileThunkFor(this, cbT); - } - - ipcMessageThunkFor(cbT: IrType & { kind: "func" }): string { - return ipcMessageThunkFor(this, cbT); - } - - ipcSendThunkFor(cbT: IrType & { kind: "func" }): string { - return ipcSendThunkFor(this, cbT); - } - - childDataThunkFor(param: IrType): string { - return childDataThunkFor(this, param); - } - - closeBindThunkFor(cbUnion: IrType, retServer: boolean): string { - return closeBindThunkFor(this, cbUnion, retServer); - } - - closeOverrideWrapFor(cbUnion: IrType, retServer: boolean): string { - return closeOverrideWrapFor(this, cbUnion, retServer); - } - - childExitSignalThunkFor(codeParam: IrType, sigParam: IrType): string { - return childExitSignalThunkFor(this, codeParam, sigParam); - } - - dgramMsgThunkFor(param: IrType): string { - return dgramMsgThunkFor(this, param); - } - - dnsLookupThunkFor(cbT: IrType): string { - return dnsLookupThunkFor(this, cbT); - } - - fsRenameThunkFor(cbT: IrType): string { - return fsRenameThunkFor(this, cbT); - } - - cryptoBytesThunkFor(cbT: IrType): string { - return cryptoBytesThunkFor(this, cbT); - } - - zlibBytesThunkFor(cbT: IrType): string { - return zlibBytesThunkFor(this, cbT); - } - - sniAnswerThunkFor(cbT: IrType): string { - return sniAnswerThunkFor(this, cbT); - } - - netLookupAnswerThunkFor(cbT: IrType): string { - return netLookupAnswerThunkFor(this, cbT); - } - - emitterInvokeThunkFor(cbT: IrType): string { - return emitterInvokeThunkFor(this, cbT); - } - - streamDataThunkFor(cbT: IrType): string { - return streamDataThunkFor(this, cbT); - } - - streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { - return streamCbThunkFor(this, kind, cbT); - } - - connectSockThunkFor(cbT: IrType): string { - return connectSockThunkFor(this, cbT); - } - connectResThunkFor(cbT: IrType): string { - return connectResThunkFor(this, cbT); - } - - raceAdapterFor(from: IrType, to: IrType): string { - return raceAdapterFor(this, from, to); - } - - resolveThunkFor(inner: IrType): string { - return resolveThunkFor(this, inner); - } -} diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts deleted file mode 100644 index a321f7d84..000000000 --- a/packages/compiler/src/backend/c/exprs.ts +++ /dev/null @@ -1,9510 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; -/* Expression C emission: the whole IrExpr dispatch (emitExpr) — every IR - * expression lands in a fresh C temp, with RC ownership tracked on the - * emitter's frames (see the discipline comment in emitter core). */ -import type { CEmitter, Temp } from "./c-emitter.js"; -import { BYTES_ELEMENT_SIZE, arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, DYN_CLASS_PROPERTIES, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js"; -import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js"; -import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js"; -import { OVERFLOW_MEMBER } from "./shapes.js"; -import { checkGlobalTdz, readBox, writeBox } from "./bindings.js"; -import { dynDestrCheckHelper, dynIterNHelper, dynKeyGetHelper, unionWidenHelper } from "./walkers.js"; -import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"; -import { genResultThunkFor } from "./async.js"; -import { isStableReceiverOperand, matchStringSelfConcat, newValueMayThrow, streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js"; - -function streamTypedRefCommitAdapter( - emitter: CEmitter, - t: IrType, - snapshot: string, - defs: string[], -): string { - if (t.kind === "bytes") { - const commit = `${snapshot}_commit`; - emitter.walkerProtos.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`, - ); - defs.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`, - ` ScrBytes *sc_target = (ScrBytes *)sc_p;`, - ` ScrBytes *sc_next = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`, - ` if (!sc_next) return;`, - ` scr_bytes_copy_contents(sc_target, sc_next);`, - ` scr_bytes_release(sc_next);`, - `}`, - ``, - ); - return `&${commit}`; - } - if (t.kind === "array") { - const commit = `${snapshot}_commit`; - emitter.walkerProtos.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`, - ); - defs.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`, - ` ScrArr *sc_target = (ScrArr *)sc_p;`, - ` ScrArr *sc_next = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`, - ` if (!sc_next) return;`, - ` size_t sc_target_len = sc_target->len;`, - ` size_t sc_target_cap = sc_target->cap;`, - ` uint64_t *sc_target_data = sc_target->data;`, - ` uint8_t *sc_target_present = sc_target->present;`, - ` ScrArrSparseSlot *sc_target_sparse = sc_target->sparse;`, - ` size_t sc_target_sparse_len = sc_target->sparse_len;`, - ` size_t sc_target_sparse_cap = sc_target->sparse_cap;`, - ` ScrArrProp *sc_target_props = sc_target->props;`, - ` size_t sc_target_prop_len = sc_target->prop_len;`, - ` size_t sc_target_prop_cap = sc_target->prop_cap;`, - ` sc_target->len = sc_next->len;`, - ` sc_target->cap = sc_next->cap;`, - ` sc_target->data = sc_next->data;`, - ` sc_target->present = sc_next->present;`, - ` sc_target->sparse = sc_next->sparse;`, - ` sc_target->sparse_len = sc_next->sparse_len;`, - ` sc_target->sparse_cap = sc_next->sparse_cap;`, - ` sc_target->props = sc_next->props;`, - ` sc_target->prop_len = sc_next->prop_len;`, - ` sc_target->prop_cap = sc_next->prop_cap;`, - ` sc_next->len = sc_target_len;`, - ` sc_next->cap = sc_target_cap;`, - ` sc_next->data = sc_target_data;`, - ` sc_next->present = sc_target_present;`, - ` sc_next->sparse = sc_target_sparse;`, - ` sc_next->sparse_len = sc_target_sparse_len;`, - ` sc_next->sparse_cap = sc_target_sparse_cap;`, - ` sc_next->props = sc_target_props;`, - ` sc_next->prop_len = sc_target_prop_len;`, - ` sc_next->prop_cap = sc_target_prop_cap;`, - ` scr_arr_release(sc_next);`, - `}`, - ``, - ); - return `&${commit}`; - } - if (isDynTypedRefType(t)) { - const meta = emitter.classMeta.get(t.className); - if (!meta) { - throw new InternalCompilerError(`emitter bug: typed-ref commit of unknown class ${t.className}`); - } - const commit = `${snapshot}_commit`; - emitter.walkerProtos.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit unknown class ${typeKey(t)} */`, - ); - defs.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`, - ` ${cDecl(t, "sc_target")} = (${cType(t).trim()})sc_p;`, - ); - for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) { - const member = mangleField(field.name); - const keyLit = cStringLiteral(Buffer.from(field.name, "utf8")); - const keyLen = Buffer.byteLength(field.name, "utf8"); - defs.push( - ` {`, - ` const ScrDyn *sc_member = scr_dyn_obj_get(sc_d, ${keyLit}, ${keyLen});`, - ` ${cDecl(field.type, "sc_next")} = ${emitter.dynCheckHelper(field.type)}(sc_member ? sc_member : scr_dyn_undefined(), NULL);`, - ` if (scr_exc_pending()) return;`, - ` ${cDecl(field.type, "sc_old")} = sc_target->${member};`, - ` sc_target->${member} = sc_next;`, - ...(isRefCounted(field.type) ? [` ${releaseCallC(field.type, "sc_old")};`] : [" (void)sc_old;"]), - ` }`, - ); - } - if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) { - const bag = `sc_target->${mangleField(DYN_CLASS_PROPERTIES)}`; - defs.push( - ` ScrDyn *sc_bag = scr_dyn_new_obj();`, - ` scr_dyn_release(scr_dyn_copy_data_properties(sc_bag, sc_d));`, - ` if (scr_exc_pending()) { scr_dyn_release(sc_bag); return; }`, - ); - for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) { - const literal = cStringLiteral(Buffer.from(field.name, "utf8")); - defs.push( - ` { ScrStr *sc_key = scr_str_new(${literal}, ${Buffer.byteLength(field.name, "utf8")});`, - ` scr_dyn_key_delete(sc_bag, sc_key, false); scr_str_release(sc_key); }`, - ); - } - defs.push(` ScrDyn *sc_old_bag = ${bag};`, ` ${bag} = sc_bag;`, ` scr_dyn_release(sc_old_bag);`); - } - defs.push(`}`, ``); - return `&${commit}`; - } - if (t.kind !== "record") return "NULL"; - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) { - throw new InternalCompilerError(`emitter bug: stream typed-ref commit of unknown shape ${t.shapeId}`); - } - const commit = `${snapshot}_commit`; - emitter.walkerProtos.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d); /* commit live stream element ${typeKey(t)} */`, - ); - defs.push( - `static void ${commit}(void *sc_p, const ScrDyn *sc_d) {`, - ` ${cDecl(t, "sc_target")} = (${cType(t).trim()})sc_p;`, - ` ${cDecl(t, "sc_next")} = ${emitter.dynCheckHelper(t)}(sc_d, NULL);`, - ` if (!sc_next) return;`, - ); - for (const field of shape.fields) { - const member = mangleField(field.name); - defs.push( - ` {`, - ` ${cDecl(field.type, "sc_old")} = sc_target->${member};`, - ` sc_target->${member} = sc_next->${member};`, - ` sc_next->${member} = sc_old;`, - ` }`, - ); - } - if (shape.indexValue) { - defs.push( - ` {`, - ` ScrMap *sc_old = sc_target->${OVERFLOW_MEMBER};`, - ` sc_target->${OVERFLOW_MEMBER} = sc_next->${OVERFLOW_MEMBER};`, - ` sc_next->${OVERFLOW_MEMBER} = sc_old;`, - ` }`, - ); - } - defs.push( - ` ${releaseCallC(t, "sc_next")};`, - `}`, - ``, - ); - return `&${commit}`; -} - -export interface StreamTypedRefAdapter { - snapshot: string; - commit: string; -} - -interface StreamTypedRefContext { - prefix: string; - defs: string[]; - adapters: Map; - unions?: Map; -} - -/** Boxes a union selected at runtime, preserving mutable/class arm identity - * while converting scalar and unit arms normally. It shares the enclosing - * adapter context so recursive class fields reuse the prototype already - * registered by streamTypedRefAdapter instead of recursing forever. */ -function nestedTypedRefUnionAdapter( - emitter: CEmitter, - t: IrType & { kind: "union" }, - ctx: StreamTypedRefContext, -): string { - const key = typeKey(t); - if (ctx.unions === undefined) ctx.unions = new Map(); - const unions = ctx.unions; - const existing = unions.get(key); - if (existing) return existing; - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: typed-ref union ${t.unionId} is undeclared`); - const sym = `${ctx.prefix}_union_${unions.size}`; - unions.set(key, sym); - emitter.walkerProtos.push(`static ScrDyn *${sym}(ScrUnion *sc_u); /* typed-ref union ${key} */`); - const lines = [`static ScrDyn *${sym}(ScrUnion *sc_u) {`, ` switch (sc_u->tag) {`]; - def.arms.forEach((arm, tag) => { - if (streamTypedRefEligible(arm) || isDynTypedRefType(arm)) { - const adapter = streamTypedRefAdapter(emitter, arm, ctx); - const rc = vAdapters(arm); - const armKey = typeKey(arm); - const keyLit = cStringLiteral(Buffer.from(armKey, "utf8")); - lines.push( - ` case ${tag}: return scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`, - ); - } else if (arm.kind === "undefinedT") { - lines.push(` case ${tag}: return scr_dyn_retain(scr_dyn_undefined());`); - } else if (arm.kind === "nullT") { - lines.push(` case ${tag}: return scr_dyn_new_null();`); - } else if (arm.kind === "f64") { - lines.push(` case ${tag}: return scr_dyn_new_num(scr_union_get_f64(sc_u));`); - } else if (arm.kind === "bool") { - lines.push(` case ${tag}: return scr_dyn_new_bool(scr_union_get_bool(sc_u));`); - } else { - lines.push(` case ${tag}: return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(sc_u));`); - } - }); - lines.push( - ` default: scr_trap("scriptc: internal error: invalid union tag\\n");`, - ` }`, - `}`, - ``, - ); - ctx.defs.push(...lines); - return sym; -} - -/** Build the live dyn view of one typed stream value. Mutable reference - * children are capsules of their own, so a write through `v.child.x` or - * `v.items[0]` commits directly to that child's static source instead of - * disappearing into the parent's detached snapshot. */ -function streamTypedRefAdapter( - emitter: CEmitter, - t: IrType, - ctx: StreamTypedRefContext, - preferredSnapshot?: string, -): StreamTypedRefAdapter { - const key = typeKey(t); - const existing = ctx.adapters.get(key); - if (existing) return existing; - const snapshot = preferredSnapshot ?? - `${ctx.prefix}_nested_${ctx.adapters.size}`; - const adapter: StreamTypedRefAdapter = { snapshot, commit: "NULL" }; - /* Register before walking children: recursive record/array types refer - * back to this prototype without recursively generating helpers. */ - ctx.adapters.set(key, adapter); - if (isDynTypedRefType(t)) { - const fields = emitter.classMeta.get(t.className)?.def.fields; - if (fields && !classDynViewSupported(fields, (id) => emitter.recordsById.get(id), (id) => emitter.unionsById.get(id))) { - adapter.snapshot = "scr_dyn_class_view_unavailable"; - return adapter; - } - } - emitter.walkerProtos.push( - `static ScrDyn *${snapshot}(void *sc_p); /* materialize live stream value ${key} */`, - ); - adapter.commit = streamTypedRefCommitAdapter(emitter, t, snapshot, ctx.defs); - - const box = (child: IrType, expr: string): string => { - if ( - child.kind === "union" && - (emitter.unionsById.get(child.unionId)?.arms.some( - (arm) => streamTypedRefEligible(arm) || isDynTypedRefType(arm), - ) ?? false) - ) { - return `${nestedTypedRefUnionAdapter(emitter, child, ctx)}(${expr})`; - } - if (!streamTypedRefEligible(child) && !isDynTypedRefType(child)) { - return `${emitter.toDynHelper(child)}(${expr})`; - } - const nested = streamTypedRefAdapter(emitter, child, ctx); - const rc = vAdapters(child); - const childKey = typeKey(child); - const keyLit = cStringLiteral(Buffer.from(childKey, "utf8")); - return `scr_dyn_new_typed_ref${emitter.traceAdapterC(child) !== null ? "_traced" : ""}(${expr}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(childKey, "utf8")}, &${nested.snapshot}, ${nested.commit})`; - }; - - const lines = [ - `static ScrDyn *${snapshot}(void *sc_p) { /* materialize live stream value ${key} */`, - ` ${cDecl(t, "v")} = (${cType(t).trim()})sc_p;`, - ]; - if (isDynTypedRefType(t)) { - const meta = emitter.classMeta.get(t.className); - if (!meta) { - throw new InternalCompilerError(`emitter bug: typed-ref materialize of unknown class ${t.className}`); - } - lines.push(` ScrDyn *d = scr_dyn_new_obj();`); - for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) { - const keyLit = cStringLiteral(Buffer.from(field.name, "utf8")); - lines.push( - ` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`, - ); - } - if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) { - lines.push(` scr_dyn_release(scr_dyn_copy_data_properties(d, v->${mangleField(DYN_CLASS_PROPERTIES)}));`); - } - lines.push(` return d;`); - } else if (t.kind === "record") { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) { - throw new InternalCompilerError( - `emitter bug: stream typed-ref materialize of unknown shape ${t.shapeId}`, - ); - } - const ordinary = shape.indexValue || shape.fields.some( - (field) => field.name === "handleEvent" && field.type.kind === "func", - ); - if (ordinary) { - lines.push(` return ${emitter.toDynHelper(t)}(v);`, `}`, ``); - ctx.defs.push(...lines); - return adapter; - } - if (shape.tuple) { - lines.push(` ScrDyn *d = scr_dyn_new_arr();`); - const fields = [...shape.fields].sort( - (a, b) => Number(a.name) - Number(b.name), - ); - for (const field of fields) { - lines.push( - ` scr_dyn_arr_push(d, ${box(field.type, `v->${mangleField(field.name)}`)});`, - ); - } - } else { - lines.push(` ScrDyn *d = scr_dyn_new_obj();`); - const byName = new Map(shape.fields.map((field) => [field.name, field])); - const order = shape.declaredOrder ?? shape.fields.map((field) => field.name); - const inOrder = new Set(order); - const fields = [ - ...order.map((name) => byName.get(name)).filter((field) => field !== undefined), - ...shape.fields.filter((field) => !inOrder.has(field.name)), - ]; - for (const field of fields) { - const keyLit = cStringLiteral(Buffer.from(field.name, "utf8")); - lines.push( - ` scr_dyn_obj_set(d, ${keyLit}, ${Buffer.byteLength(field.name, "utf8")}, ${box(field.type, `v->${mangleField(field.name)}`)});`, - ); - } - } - lines.push(` return d;`); - } else if (t.kind === "array") { - const elem = t.elem; - lines.push( - ` ScrDyn *d = scr_dyn_new_arr();`, - ` for (size_t sc_i = 0; sc_i < v->len; sc_i++) {`, - ); - if (elem.kind === "f64") { - lines.push( - ` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_f64(v, (double)sc_i)")});`, - ); - } else if (elem.kind === "bool") { - lines.push( - ` scr_dyn_arr_push(d, ${box(elem, "scr_arr_get_bool(v, (double)sc_i)")});`, - ); - } else { - lines.push( - ` ${cDecl(elem, "sc_value")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)sc_i);`, - ` scr_dyn_arr_push(d, ${box(elem, "sc_value")});`, - ` ${releaseCallC(elem, "sc_value")};`, - ); - } - lines.push(` }`, ` return d;`); - } else { - lines.push(` return ${emitter.toDynHelper(t)}(v);`); - } - lines.push(`}`, ``); - ctx.defs.push(...lines); - return adapter; -} - -/** One per-type capsule adapter for Web API arguments whose JavaScript - * contract preserves the exact input reference. */ -export function liveDynRefAdapter( - emitter: CEmitter, - t: IrType, -): StreamTypedRefAdapter { - const key = typeKey(t); - const existing = emitter.liveDynRefAdapters.get(key); - if (existing) return existing; - if (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) { - throw new InternalCompilerError(`emitter bug: live dyn ref of ${key}`); - } - const prefix = `sc_ldr_${emitter.liveDynRefAdapters.size}`; - const defs: string[] = []; - const adapter = streamTypedRefAdapter( - emitter, - t, - { prefix, defs, adapters: new Map() }, - `${prefix}_materialize`, - ); - emitter.liveDynRefAdapters.set(key, adapter); - emitter.walkerDefs.push(...defs); - return adapter; -} - -/** A union itself is only the tagged box; the identity Web APIs expose is - * the selected mutable arm. Dispatch on the tag and point the typed capsule - * directly at that arm's payload. Non-mutable arms keep ordinary dynFrom - * semantics. */ -function liveDynUnionRefAdapter( - emitter: CEmitter, - t: IrType & { kind: "union" }, -): string { - const key = typeKey(t); - const existing = emitter.liveDynUnionRefAdapters.get(key); - if (existing) return existing; - const union = emitter.unionsById.get(t.unionId); - if (!union) { - throw new InternalCompilerError(`emitter bug: live dyn ref of unknown union ${t.unionId}`); - } - const mutableArms = union.arms - .map((arm, tag) => ({ arm, tag })) - .filter(({ arm }) => streamTypedRefEligible(arm) || isDynTypedRefType(arm)); - if (mutableArms.length === 0) { - throw new InternalCompilerError(`emitter bug: live dyn ref of immutable union ${key}`); - } - - const sym = `sc_ldu_${emitter.liveDynUnionRefAdapters.size}`; - emitter.liveDynUnionRefAdapters.set(key, sym); - const sig = `static ScrDyn *${sym}(ScrUnion *sc_u)`; - emitter.walkerProtos.push(`${sig}; /* materialize live union value ${key} */`); - const defs: string[] = []; - const adapters = new Map(); - for (const { arm, tag } of mutableArms) { - const prefix = `${sym}_${tag}`; - adapters.set( - tag, - streamTypedRefAdapter( - emitter, - arm, - { prefix, defs, adapters: new Map() }, - `${prefix}_materialize`, - ), - ); - } - - defs.push(`${sig} {`, ` switch (sc_u->tag) {`); - for (const { arm, tag } of mutableArms) { - const adapter = adapters.get(tag)!; - const rc = vAdapters(arm); - const armKey = typeKey(arm); - const keyLit = cStringLiteral(Buffer.from(armKey, "utf8")); - defs.push( - ` case ${tag}:`, - ` return scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_u), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`, - ); - } - const mutableTags = new Set(mutableArms.map(({ tag }) => tag)); - union.arms.forEach((arm, tag) => { - if (mutableTags.has(tag)) return; - if (arm.kind === "undefinedT") { - defs.push(` case ${tag}: return scr_dyn_retain(scr_dyn_undefined());`); - } else if (arm.kind === "nullT") { - defs.push(` case ${tag}: return scr_dyn_new_null();`); - } else if (arm.kind === "f64") { - defs.push(` case ${tag}: return scr_dyn_new_num(scr_union_get_f64(sc_u));`); - } else if (arm.kind === "bool") { - defs.push(` case ${tag}: return scr_dyn_new_bool(scr_union_get_bool(sc_u));`); - } else { - defs.push(` case ${tag}: return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(sc_u));`); - } - }); - defs.push( - ` default: scr_trap("scriptc: internal error: invalid union tag\\n");`, - ` }`, - `}`, - ``, - ); - emitter.walkerDefs.push(...defs); - return sym; -} - -function streamFromArrayAdapter( - emitter: CEmitter, - t: IrType & { kind: "array" }, -): string { - const elem = t.elem; - const key = typeKey(elem); - const existing = emitter.streamFromArrayAdapters.get(key); - if (existing) return existing; - const sym = `sc_sfa_${emitter.streamFromArrayAdapters.size}`; - emitter.streamFromArrayAdapters.set(key, sym); - const sig = `static ScrDyn *${sym}(ScrArr *sc_a, double sc_i)`; - emitter.walkerProtos.push(`${sig}; /* ReadableStream.from array<${key}> */`); - const unionDef = elem.kind === "union" - ? emitter.unionsById.get(elem.unionId) - : undefined; - if (elem.kind === "union" && !unionDef) { - throw new InternalCompilerError(`emitter bug: streamFrom of unknown union ${elem.unionId}`); - } - const unionRefArms = unionDef?.arms - .map((arm, tag) => ({ arm, tag })) - .filter(({ arm }) => - isRefCounted(arm) && arm.kind !== "dyn" && arm.kind !== "string" - ) ?? []; - const typedRef = isRefCounted(elem) && - elem.kind !== "dyn" && - elem.kind !== "string" && - elem.kind !== "union"; - const snapshot = `${sym}_materialize`; - const d: string[] = []; - let commit = "NULL"; - if (typedRef) { - if (streamTypedRefEligible(elem)) { - const adapter = streamTypedRefAdapter( - emitter, - elem, - { prefix: snapshot, defs: d, adapters: new Map() }, - snapshot, - ); - commit = adapter.commit; - } else { - const snapshotSig = `static ScrDyn *${snapshot}(void *sc_p)`; - emitter.walkerProtos.push(`${snapshotSig}; /* materialize stream element ${key} */`); - d.push( - `${snapshotSig} {`, - ` return ${emitter.toDynHelper(elem)}((${cType(elem).trim()})sc_p);`, - `}`, - ``, - ); - commit = streamTypedRefCommitAdapter(emitter, elem, snapshot, d); - } - } - const unionCommits = new Map(); - for (const { arm, tag } of unionRefArms) { - const armKey = typeKey(arm); - const armSnapshot = `${snapshot}_${tag}`; - const snapshotSig = `static ScrDyn *${armSnapshot}(void *sc_p)`; - emitter.walkerProtos.push(`${snapshotSig}; /* materialize stream union arm ${armKey} */`); - d.push( - `${snapshotSig} {`, - ` return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})sc_p);`, - `}`, - ``, - ); - unionCommits.set( - tag, - streamTypedRefCommitAdapter(emitter, arm, armSnapshot, d), - ); - } - d.push(`${sig} { /* ReadableStream.from array<${key}> */`); - if (elem.kind === "f64") { - d.push( - ` return ${emitter.toDynHelper(elem)}(scr_arr_get_f64(sc_a, sc_i));`, - ); - } else if (elem.kind === "bool") { - d.push( - ` return ${emitter.toDynHelper(elem)}(scr_arr_get_bool(sc_a, sc_i));`, - ); - } else { - d.push(` ${cDecl(elem, "sc_v")} = (${cType(elem).trim()})scr_arr_get_ref(sc_a, sc_i);`); - if (elem.kind === "union") { - d.push(` ScrDyn *sc_d;`, ` switch (sc_v->tag) {`); - for (const { arm, tag } of unionRefArms) { - const armKey = typeKey(arm); - const rc = vAdapters(arm); - const keyLit = cStringLiteral(Buffer.from(armKey, "utf8")); - d.push( - ` case ${tag}:`, - ` sc_d = scr_dyn_new_typed_ref${emitter.traceAdapterC(arm) !== null ? "_traced" : ""}(scr_union_peek(sc_v), &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(armKey, "utf8")}, &${snapshot}_${tag}, ${unionCommits.get(tag) ?? "NULL"});`, - ` break;`, - ); - } - d.push( - ` default:`, - ` sc_d = ${emitter.toDynHelper(elem)}(sc_v);`, - ` break;`, - ` }`, - ); - } else if (typedRef) { - const rc = vAdapters(elem); - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - const keyLen = Buffer.byteLength(key, "utf8"); - d.push( - ` ScrDyn *sc_d = scr_dyn_new_typed_ref${emitter.traceAdapterC(elem) !== null ? "_traced" : ""}(sc_v, &${rc.retain}, &${rc.release}, ${keyLit}, ${keyLen}, &${snapshot}, ${commit});`, - ); - } else { - d.push(` ScrDyn *sc_d = ${emitter.toDynHelper(elem)}(sc_v);`); - } - d.push( - ` ${releaseCallC(elem, "sc_v")};`, - ` return sc_d;`, - ); - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return sym; -} - -function dynPromiseAdapter( - emitter: CEmitter, - inner: IrType, -): string { - if (!isRefCounted(inner) || inner.kind === "dyn") { - throw new InternalCompilerError( - `dynamic promise adapter requires a concrete reference type, got ${typeKey(inner)}`, - ); - } - const key = typeKey(inner); - const existing = emitter.dynPromiseAdapters.get(key); - if (existing) return existing; - const sym = `sc_dpa_${emitter.dynPromiseAdapters.size}`; - emitter.dynPromiseAdapters.set(key, sym); - const sig = `static void ${sym}(ScrPromise *sc_dst, ScrPromise *sc_src)`; - emitter.walkerProtos.push(`${sig}; /* checked-dynamic promise exit ${key} */`); - let fulfill: string; - if (inner.kind === "string") { - fulfill = `scr_promise_fulfill_str(sc_dst, sc_v);`; - } else { - const rc = vAdapters(inner); - fulfill = `scr_promise_fulfill_ref(sc_dst, sc_v, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(inner)});`; - } - emitter.walkerDefs.push( - `${sig} { /* checked-dynamic promise exit ${key} */`, - ` ScrDyn *sc_d = (ScrDyn *)scr_promise_payload_ref(sc_src);`, - ` ${cDecl(inner, "sc_v")} = ${emitter.dynCheckHelper(inner)}(sc_d, NULL);`, - ` scr_dyn_release(sc_d);`, - ` if (scr_exc_pending()) {`, - ` scr_promise_reject_pending(sc_dst);`, - ` return;`, - ` }`, - ` ${fulfill}`, - `}`, - ``, - ); - return sym; -} - - - - - - -/** Evaluate a receiver as a borrow when it is a direct, unboxed - * binding and all later operands are stable. The binding's scope/global - * owner then keeps the value alive, avoiding retain/release traffic around - * every indexed access. Any uncertain shape falls back to an owned temp. */ -export function emitStableReceiver(emitter: CEmitter, receiver: IrExpr, following: IrExpr[]): Temp { - if ( - receiver.kind === "varRef" && - following.every((operand) => isStableReceiverOperand(operand, receiver.localId)) - ) { - const local = emitter.currentLocals.get(receiver.localId); - if (local && !local.boxed) { - return emitter.newBorrowedTemp(receiver.type, mangleLocal(receiver.localId)); - } - if (!local && emitter.globalsById.has(receiver.localId)) { - checkGlobalTdz(emitter, emitter.globalsById.get(receiver.localId)!); - return emitter.newBorrowedTemp(receiver.type, mangleGlobal(receiver.localId)); - } - } - return emitter.emitExpr(receiver); -} - -function emitMapLikeIntrinsic( - emitter: CEmitter, - e: Extract, -): Temp { - const r = emitter.emitExpr(e.receiver); - const receiverType = e.receiver.type; - if (e.kind === "mapIntrinsic" && receiverType.kind !== "map") { - throw new InternalCompilerError("emitter bug: mapIntrinsic on non-map"); - } - if (e.kind === "setIntrinsic" && receiverType.kind !== "set") { - throw new InternalCompilerError("emitter bug: setIntrinsic on non-set"); - } - if (receiverType.kind !== "map" && receiverType.kind !== "set") { - throw new InternalCompilerError("unreachable"); - } - const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem; - const kAcc = mapKeyAccess(key); - const method = e.method; - switch (method) { - case "get": { - if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable"); - const value = receiverType.value; - const vAcc = elemAccess(value); - // The union construction is type-directed HERE, like - // process.envGet — the runtime knows no tags. Ref values come - // back +1 (ownership MOVES into the fresh union box on a hit); - // scalars ride an out-param behind a found flag; a miss is the - // interned immortal undefined-arm instance. When V is itself a - // union, the stored box IS the result: `undefined` sorts last - // in canonical arm order, so V's tags coincide with the result - // union's and no re-tag exists (validated). - const k = emitter.emitExpr(e.args[0]!); - if (value.kind === "dyn") { - const t = emitter.newTemp(e.type, `(ScrDyn *)scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`); - emitter.line(`if (!${t.name}) ${t.name} = scr_dyn_undefined();`); - return t; - } - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: map get result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (!def || undefTag < 0) throw new InternalCompilerError("emitter bug: map get union lacks its undefined arm"); - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - if (value.kind === "union") { - const t = emitter.newTemp( - e.type, - `(ScrUnion *)scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`, - ); - emitter.line(`if (!${t.name}) ${t.name} = ${absent};`); - return t; - } - const valueTag = def.arms.findIndex((a) => typeEquals(a, value)); - if (valueTag < 0) throw new InternalCompilerError("emitter bug: map get union lacks its value arm"); - if (value.kind === "f64" || value.kind === "bool") { - const out = `sc_t${emitter.tempCounter++}`; - const found = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(value, out)} = 0;`); - emitter.line(`bool ${found} = scr_map_get_${kAcc}_${vAcc}(${r.name}, ${k.name}, &${out});`); - const make = `scr_union_new_${value.kind}(${valueTag}, ${out})`; - return emitter.newTemp(e.type, `${found} ? ${make} : ${absent}`); - } - const rc = vAdapters(value); - const v = emitter.newTemp(value, `(${cType(value).trim()})scr_map_get_${kAcc}_ref(${r.name}, ${k.name})`); - emitter.moveTemp(v); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${valueTag}, ${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(value)})`; - return emitter.newTemp(e.type, `${v.name} ? ${present} : ${absent}`); - } - case "set": { - if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable"); - // Key borrowed (the runtime retains stored string keys); the - // value MOVES in (replacement releases the old value inside). - const k = emitter.emitExpr(e.args[0]!); - const v = emitter.emitExpr(e.args[1]!); - const vAcc = elemAccess(receiverType.value); - if (vAcc === "ref") emitter.moveTemp(v); - emitter.line(`scr_map_set_${kAcc}_${vAcc}(${r.name}, ${k.name}, ${v.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "add": { - if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable"); - // Element borrowed (the runtime retains stored strings); the unit - // value is 0. Re-adding overwrites in place, preserving insertion. - const k = emitter.emitExpr(e.args[0]!); - emitter.line(`scr_map_set_${kAcc}_f64(${r.name}, ${k.name}, 0);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "has": { - const k = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_map_has_${kAcc}(${r.name}, ${k.name})`); - } - case "delete": { - const k = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_map_delete_${kAcc}(${r.name}, ${k.name})`); - } - case "size": - return emitter.newTemp(e.type, `scr_map_size(${r.name})`); - case "clear": - emitter.line(`scr_map_clear(${r.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "iterCount": - return emitter.newTemp(e.type, `scr_map_iter_count(${r.name})`); - case "iterLive": { - const i = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_map_iter_live(${r.name}, ${i.name})`); - } - case "iterKey": { - // String/ref keys and elements come back +1. - const i = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_map_iter_key_${kAcc}(${r.name}, ${i.name})`); - } - case "iterValue": { - if (receiverType.kind !== "map") throw new InternalCompilerError("unreachable"); - const value = receiverType.value; - const vAcc = elemAccess(value); - const i = emitter.emitExpr(e.args[0]!); - const read = - vAcc === "ref" - ? `(${cType(value).trim()})scr_map_iter_val_ref(${r.name}, ${i.name})` - : `scr_map_iter_val_${vAcc}(${r.name}, ${i.name})`; - return emitter.newTemp(e.type, read); - } - case "iterEnter": - emitter.line(`scr_map_iter_enter(${r.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "iterExit": - emitter.line(`scr_map_iter_exit(${r.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "toArray": { - if (receiverType.kind !== "set") throw new InternalCompilerError("unreachable"); - // Fresh +1 elem[] of the live entries in insertion order. - return emitter.newTemp(e.type, `scr_set_to_arr_${kAcc}(${r.name})`); - } - default: { - const _exhaustive: never = method; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -type ExprOf = Extract; - -function emitLiteralExpr( - emitter: CEmitter, - e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">, -): Temp { - switch (e.kind) { - case "numLit": - return emitter.newTemp(e.type, cNumberLiteral(e.value)); - case "boolLit": - return emitter.newTemp(e.type, e.value ? "true" : "false"); - case "strLit": { - const sym = emitter.internLiteral(e.value); - return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`)); - } - case "moduleNsRef": { - const sym = emitter.internLiteral(`module:${e.moduleId}`); - return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`)); - } - case "unitLit": - // unitLits are consumed inline by the unionWrap case (a unit arm is - // tag-only); one reaching the generic dispatch escaped its wrap. - throw new InternalCompilerError(`emitter bug: bare unitLit '${e.unit}'`); - case "varRef": { - const integerLoopIndex = emitter.integerLoopIndex(e); - if (integerLoopIndex !== null) return emitter.newTemp(e.type, `(double)${integerLoopIndex}`); - const local = emitter.currentLocals.get(e.localId); - if (!local && emitter.globalsById.has(e.localId)) { - checkGlobalTdz(emitter, emitter.globalsById.get(e.localId)!); - const gname = mangleGlobal(e.localId); - return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, gname) : gname); - } - const name = mangleLocal(e.localId); - if (local?.boxed) { - return emitter.newTemp(e.type, readBox(emitter, local)); - } - return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, name) : name); - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -/** Unsigned operations avoid signed overflow and implementation-defined - * right shifts/conversions. Reinterpret the final bits numerically. */ -function emitIntegerBits(emitter: CEmitter, e: ExprOf<"bin" | "unary">, left: string, right?: string): Temp { - const a = `sc_bits_${emitter.tempCounter++}`; - const out = `sc_bits_${emitter.tempCounter++}`; - emitter.line(`uint32_t ${a} = (uint32_t)(int64_t)${left};`); - let operation: string; - if (e.kind === "unary") operation = `~${a}`; - else { - const b = `sc_bits_${emitter.tempCounter++}`; - const shift = e.op === "<<" || e.op === ">>" || e.op === ">>>"; - emitter.line(`uint32_t ${b} = (uint32_t)(int64_t)${right}${shift ? " & 31u" : ""};`); - operation = e.op === ">>" ? `(${a} >> ${b}) | ((${a} & 0x80000000u) ? ~(UINT32_MAX >> ${b}) : 0u)` - : `${a} ${e.op === ">>>" ? ">>" : e.op} ${b}`; - } - emitter.line(`uint32_t ${out} = ${operation};`); - return emitter.newTemp(e.type, e.op === ">>>" ? `(double)${out}` : `(double)${out} - ((${out} & 0x80000000u) ? 4294967296.0 : 0.0)`); -} - -function emitOperatorExpr( - emitter: CEmitter, - e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">, -): Temp { - switch (e.kind) { - case "bin": { - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - if ((e.op === "+" || e.op === "-") && emitter.integerRanges.get(e)) { - return emitter.newTemp(e.type, `(double)((int64_t)${l.name} ${e.op} (int64_t)${r.name})`); - } - if (["&", "|", "^", "<<", ">>", ">>>"].includes(e.op) && emitter.integerRanges.get(e.left) && emitter.integerRanges.get(e.right)) { - return emitIntegerBits(emitter, e, l.name, r.name); - } - switch (e.op) { - case "%": - return emitter.newTemp(e.type, `fmod(${l.name}, ${r.name})`); - case "**": - return emitter.newTemp(e.type, `pow(${l.name}, ${r.name})`); - case "===": - return emitter.newTemp(e.type, `${l.name} == ${r.name}`); - case "!==": - return emitter.newTemp(e.type, `${l.name} != ${r.name}`); - // The bitwise six ride runtime helpers (ToInt32/ToUint32 wrap, - // 5-bit shift masks, exact C-portable arithmetic shifts). - case "&": - return emitter.newTemp(e.type, `scr_bit_and(${l.name}, ${r.name})`); - case "|": - return emitter.newTemp(e.type, `scr_bit_or(${l.name}, ${r.name})`); - case "^": - return emitter.newTemp(e.type, `scr_bit_xor(${l.name}, ${r.name})`); - case "<<": - return emitter.newTemp(e.type, `scr_bit_shl(${l.name}, ${r.name})`); - case ">>": - return emitter.newTemp(e.type, `scr_bit_shr(${l.name}, ${r.name})`); - case ">>>": - return emitter.newTemp(e.type, `scr_bit_ushr(${l.name}, ${r.name})`); - default: - return emitter.newTemp(e.type, `${l.name} ${e.op} ${r.name}`); - } - } - case "unary": { - const v = emitter.emitExpr(e.operand); - if (e.op === "~") return emitter.integerRanges.get(e.operand) - ? emitIntegerBits(emitter, e, v.name) : emitter.newTemp(e.type, `scr_bit_not(${v.name})`); - return emitter.newTemp(e.type, `${e.op}${v.name}`); - } - case "incDec": { - // Expression-position ++/--: read, write ±1, yield old (postfix) - // or new (prefix) — the yielded value is snapshotted into its own - // temp, so later writes to the binding can't disturb it. Receivers - // are always f64 (frontend fence), so no RC bookkeeping. - const local = emitter.currentLocals.get(e.localId); - const one = e.op === "+" ? "+ 1" : "- 1"; - if (local?.boxed) { - const old = emitter.newTemp(e.type, readBox(emitter, local)); - if (e.prefix) { - const t = emitter.newTemp(e.type, `${old.name} ${one}`); - writeBox(emitter, local, t.name); - return t; - } - writeBox(emitter, local, `${old.name} ${one}`); - return old; - } - if (!local && !emitter.globalsById.has(e.localId)) { - throw new InternalCompilerError(`emitter bug: incDec of unknown binding ${e.localId}`); - } - const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId); - const t = emitter.newTemp(e.type, e.prefix ? `${target} ${one}` : target); - emitter.line(`${target} = ${e.prefix ? t.name : `${t.name} ${one}`};`); - return t; - } - case "fieldIncDec": { - // ++/-- over a class FIELD in expression position: one receiver - // evaluation, read-modify-write, old/new snapshotted into temps - // like the local form. f64 fields compute in place; CHECKED- - // DYNAMIC fields validate the number OUT (dynCheck — the catchable - // TypeError on non-numbers), compute, and box the result back into - // the slot. Unlink-then-release like fieldSet: the old box leaves - // the heap edge before its count is given up (scr_cycle.c). - const obj = emitter.emitExpr(e.obj); - const field = `${obj.name}->${mangleField(e.field)}`; - const one = e.op === "+" ? "+ 1" : "- 1"; - if (!e.fieldDyn) { - const old = emitter.newTemp(e.type, field); - if (e.prefix) { - const t = emitter.newTemp(e.type, `${old.name} ${one}`); - emitter.line(`${field} = ${t.name};${emitter.srcComment(e.loc)}`); - return t; - } - emitter.line(`${field} = ${old.name} ${one};${emitter.srcComment(e.loc)}`); - return old; - } - const helper = emitter.dynCheckHelper(e.type); - const old = emitter.fallibleTemp(e.type, `${helper}(${field}, NULL)`); - const next = emitter.newTemp(e.type, `${old.name} ${one}`); - const oldBox = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrDyn *${oldBox} = ${field};`); - emitter.line(`${field} = scr_dyn_new_num(${next.name});${emitter.srcComment(e.loc)}`); - emitter.releaseValue(oldBox, DYN); - return e.prefix ? next : old; - } - case "assignExpr": { - // `x = e` in expression position: e evaluates once, the binding takes - // its OWN reference (retain for ref kinds — the frame keeps the temp's - // reference for the yielded value, released at statement end as usual), - // and the temp is the expression's value. Mirrors the `assign` - // statement's old-value release / boxed-set behavior exactly. - const local = emitter.currentLocals.get(e.localId); - const concat = e.value; - const suffix = matchStringSelfConcat(e.localId, concat); - if (suffix && concat.kind === "strConcat") { - // Keep the old left value alive across suffix evaluation, then - // detach whichever value the binding holds at that point. This is - // the expression-position twin of stmt assign's ownership handoff. - const snapshot = emitter.emitExpr(concat.left); - const right = emitter.emitExpr(suffix); - if (!local && !emitter.globalsById.has(e.localId)) { - throw new InternalCompilerError(`emitter bug: assignExpr to unknown binding ${e.localId}`); - } - const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId); - if (local?.boxed) { - emitter.line(`scr_box_set_ref(${target}, NULL);`); - } else { - const old = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrStr *${old} = ${target};`); - emitter.line(`${target} = NULL;`); - emitter.releaseValue(old, snapshot.type); - } - const result = emitter.newTemp(e.type, `scr_str_concat(${snapshot.name}, ${right.name})`); - // An assignment expression yields its own +1, so give the binding - // a retained sibling reference rather than moving result out. - const stored = retainCallC(result.type, result.name); - if (local?.boxed) { - emitter.line(`scr_box_set_ref(${target}, ${stored});`); - } else { - emitter.line(`${target} = ${stored};`); - } - return result; - } - const v = emitter.emitExpr(e.value); - if (local?.boxed) { - // box_set takes ownership of the passed reference, so hand it a - // retained copy and keep the temp's own reference for the yield. - const stored = isRefCounted(v.type) ? retainCallC(v.type, v.name) : v.name; - writeBox(emitter, local, stored); - return v; - } - if (!local && !emitter.globalsById.has(e.localId)) { - throw new InternalCompilerError(`emitter bug: assignExpr to unknown binding ${e.localId}`); - } - if (!local) checkGlobalTdz(emitter, emitter.globalsById.get(e.localId)!); - const target = local ? mangleLocal(e.localId) : mangleGlobal(e.localId); - if (isRefCounted(v.type)) { - // Old-value release is NULL-tolerant for globals (statics start - // NULL); locals always hold a live value by definite assignment. - emitter.releaseValue(target, v.type); - emitter.line(`${target} = ${retainCallC(v.type, v.name)};`); - } else { - emitter.line(`${target} = ${v.name};`); - } - return v; - } - case "seqExpr": { - // Keep saved operands alive through the enclosing expression: - // tuple spreads and JS arguments reuse them in later call slots. - // The current frame still cleans up on the path that created them, - // including a lazy branch that is skipped on later loop iterations. - emitter.scopes.push([]); - for (const s of e.stmts) emitter.emitStmt(s); - const result = emitter.emitExpr(e.result); - emitter.currentFrame().push(...emitter.scopes.pop()!); - return result; - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitControlExpr( - emitter: CEmitter, - e: ExprOf<"dynDestrCheck" | "dynIterN" | "toBool" | "logical" | "ternary" | "optChain" | "chainRecv" | "orDefault" | "nullish">, -): Temp { - switch (e.kind) { - case "dynDestrCheck": { - // RequireObjectCoercible with V8's destructuring TypeError. dyn - // values check in the runtime helper and pass through unchanged - // (same temp, same ownership); island values check in the engine - // (a fresh +1 cell for the same value comes back). - const v = emitter.emitExpr(e.value); - const first = e.firstProp !== undefined ? cStringLiteral(Buffer.from(e.firstProp, "utf8")) : "NULL"; - const spell = cStringLiteral(Buffer.from(e.spelling, "utf8")); - if (e.value.type.kind === "jsval") { - return emitter.fallibleTemp(e.type, `scr_jsval_destr_check(${v.name}, ${spell}, ${first})`); - } - const helper = dynDestrCheckHelper(emitter); - emitter.line(`${helper}(${v.name}, ${spell}, ${first});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return v; - } - case "dynIterN": { - // GetIterator + first-N steps as a fresh array (V8's exact - // not-iterable TypeError on non-iterables): the dyn helper for - // dyn operands, the engine's real iterator protocol for island - // ones. - const v = emitter.emitExpr(e.value); - if (e.value.type.kind === "jsval") { - return emitter.fallibleTemp(e.type, `scr_jsval_iter_n(${v.name}, ${e.count})`); - } - const helper = dynIterNHelper(emitter); - return emitter.fallibleTemp(e.type, `${helper}(${v.name}, ${e.count})`); - } - case "toBool": { - // JS ToBoolean. The operand temp (for strings: owned by the current - // frame) is only read here and released at statement end as usual. - const v = emitter.emitExpr(e.operand); - return emitter.newTemp(e.type, emitter.truthyC(v)); - } - case "logical": { - // JS value semantics: `a && b` ≡ `toBool(a) ? b : a`, `a || b` ≡ - // `toBool(a) ? a : b` — the result is an operand value, not a bool. - // The left operand evaluates exactly once and its ownership moves - // into the result temp; when the branch takes the right operand - // instead, the stale left value is released first and the right - // operand runs in its own frame (its leftover temps release inside - // the branch). Same move/release dance as `ternary`. - const l = emitter.emitExpr(e.left); - emitter.moveTemp(l); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)} = ${l.name};`); - const truthy = emitter.truthyC({ name, type: e.type }); - emitter.line(`if (${e.op === "&&" ? truthy : `!(${truthy})`}) {`); - emitter.indent++; - if (isRefCounted(e.type)) emitter.releaseValue(name, e.type); - emitter.emitBranchInto(name, e.right); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "ternary": { - // Exactly one arm evaluates. Each arm runs in its own frame: the - // chosen value's ownership moves to the result temp, everything - // else the arm allocated is released inside its branch. - const c = emitter.emitExpr(e.cond); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - const emitArm = (arm: IrExpr) => { - emitter.indent++; - emitter.emitBranchInto(name, arm); - emitter.indent--; - }; - emitter.line(`if (${c.name}) {`); - emitArm(e.then); - emitter.line(`} else {`); - emitArm(e.else_); - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "optChain": { - // `a?.b` / `f?.()`: the nullish test inverted. The receiver - // evaluates once into an ordinary frame temp (borrowed — the frame - // releases it at statement end). On a unit tag the result is the - // interned undefined arm and the body never runs (argument side - // effects included); otherwise the narrowed payload fills the bind - // temp (+1, frame-owned, NULL-initialized so the unit path's frame - // release is a no-op) and the body reads it through chainRecv. - // Island-handle chain: the nullish test asks the engine value; the - // unit path result is the engine's undefined (+1 cell), the body - // runs lazily over the bound handle otherwise. - if (e.receiver.type.kind === "jsval") { - const r = emitter.emitExpr(e.receiver); - const bind = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, bind)} = NULL;`); - emitter.currentFrame().push({ name: bind, type: e.type }); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (scr_jsval_is_nullish(${r.name})) {`); - emitter.indent++; - emitter.line(`${name} = scr_jsval_undefined();`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${bind} = scr_jsval_retain(${r.name});`); - emitter.chainTemps.set(e.id, { name: bind, type: e.type }); - emitter.emitBranchInto(name, e.body); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - // A dyn (dyn) receiver — the `rawName?.match(re)` step: the nullish - // test reads the node's kind tag; the unit path is the undefined - // dyn singleton (dyn results) or nothing (void bodies), the body - // runs over the bound receiver otherwise (the validated dynamic - // dispatch, its result converted back into the checked-dynamic tree by the - // frontend's dynFrom wrap). - if (e.receiver.type.kind === "dyn") { - const r = emitter.emitExpr(e.receiver); - const test = `(${r.name}->kind == SCR_DYN_UNDEF || ${r.name}->kind == SCR_DYN_NULL)`; - const bind = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.receiver.type, bind)} = NULL;`); - emitter.currentFrame().push({ name: bind, type: e.receiver.type }); - if (e.type.kind === "void") { - emitter.line(`if (!${test}) {`); - emitter.indent++; - emitter.line(`${bind} = scr_dyn_retain(${r.name});`); - emitter.chainTemps.set(e.id, { name: bind, type: e.receiver.type }); - emitter.frames.push([]); - emitter.emitExpr(e.body); - emitter.releaseFrame(emitter.frames.pop()!); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - return { name: "", type: e.type }; - } - if (e.type.kind !== "dyn") throw new InternalCompilerError("emitter bug: dyn optChain result kind"); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (${test}) {`); - emitter.indent++; - emitter.line(`${name} = scr_dyn_retain(scr_dyn_undefined());`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${bind} = scr_dyn_retain(${r.name});`); - emitter.chainTemps.set(e.id, { name: bind, type: e.receiver.type }); - emitter.emitBranchInto(name, e.body); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - if (e.receiver.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain receiver is not a union"); - const def = emitter.unionsById.get(e.receiver.type.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: optChain of unknown union ${e.receiver.type.unionId}`); - const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : [])); - const narrowIdx = def.arms.findIndex((a) => !isUnitType(a)); - if (unitTags.length === 0 || narrowIdx < 0) throw new InternalCompilerError("emitter bug: optChain union arms"); - const multiple = def.arms.length - unitTags.length > 1; - const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!; - const r = emitter.emitExpr(e.receiver); - const bind = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(narrowed, bind)} = ${isRefCounted(narrowed) ? "NULL" : "0"};`); - if (isRefCounted(narrowed)) emitter.currentFrame().push({ name: bind, type: narrowed }); - const test = unitTags.map((t) => `${r.name}->tag == ${t}`).join(" || "); - const extract = multiple - ? retainCallC(narrowed, r.name) - : narrowed.kind === "f64" - ? `scr_union_get_f64(${r.name})` - : narrowed.kind === "bool" - ? `scr_union_get_bool(${r.name})` - : retainCallC(narrowed, `(${cType(narrowed).trim()})scr_union_peek(${r.name})`); - if (e.type.kind === "void") { - // Statement form (cb?.()): no result value at all. - emitter.line(`if (!(${test})) {`); - emitter.indent++; - emitter.line(`${bind} = ${extract};`); - emitter.chainTemps.set(e.id, { name: bind, type: narrowed }); - emitter.frames.push([]); - emitter.emitExpr(e.body); - emitter.releaseFrame(emitter.frames.pop()!); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - return { name: "", type: e.type }; - } - // A dyn-typed chain (`pricing?.[key]` over an unknown-valued index - // signature): the unit path is the undefined dyn value — dyn - // represents undefined directly, no union wrapper exists. - if (e.type.kind === "dyn") { - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (${test}) {`); - emitter.indent++; - emitter.line(`${name} = scr_dyn_retain(scr_dyn_undefined());`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${bind} = ${extract};`); - emitter.chainTemps.set(e.id, { name: bind, type: narrowed }); - emitter.emitBranchInto(name, e.body); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain result is not a union"); - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (undefTag < 0) throw new InternalCompilerError("emitter bug: optChain result lacks its undefined arm"); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (${test}) {`); - emitter.indent++; - emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, undefTag)};`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${bind} = ${extract};`); - emitter.chainTemps.set(e.id, { name: bind, type: narrowed }); - emitter.emitBranchInto(name, e.body); - emitter.chainTemps.delete(e.id); - emitter.indent--; - emitter.line(`}`); - emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "chainRecv": { - const bound = emitter.chainTemps.get(e.id); - if (!bound) throw new InternalCompilerError(`emitter bug: chainRecv "${e.id}" outside its chain`); - return emitter.newTemp( - e.type, - isRefCounted(e.type) ? retainCallC(e.type, bound.name) : bound.name, - ); - } - case "orDefault": { - // `u || d` narrowed to the single non-unit arm: nullish's dance - // with the per-union TRUTHY helper as the test — truthy extracts - // the arm (+1 for ref kinds), falsy releases and runs the default. - if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: orDefault left is not a union"); - const l = emitter.emitExpr(e.left); - emitter.moveTemp(l); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (${emitter.unionTruthyHelper(e.left.type.unionId)}(${l.name})) {`); - emitter.indent++; - if (e.retag !== undefined) { - // The helper CONSUMES the left box (callees own their params), so - // no release on this side — the falsy side still owns and frees it. - // An unwind here produces the NULL dummy and never reaches the - // frame push below, so the slot needs no early registration. - emitter.line(`${name} = ${emitter.callTargetC(e.retag)}(${l.name});`); - if (emitter.mayThrow.has(e.retag)) emitter.emitPendingCheck(); - } else { - const arm = e.type; - const read = - arm.kind === "f64" - ? `scr_union_get_f64(${l.name})` - : arm.kind === "bool" - ? `scr_union_get_bool(${l.name})` - : retainCallC(arm, `(${cType(arm).trim()})scr_union_peek(${l.name})`); - emitter.line(`${name} = ${read};`); - emitter.releaseValue(l.name, e.left.type); - } - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.releaseValue(l.name, e.left.type); - emitter.emitBranchInto(name, e.right); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "nullish": { - // `a ?? b`: logical's move/release dance with the left's runtime - // TAG against its unit arms as the test. The left evaluates exactly - // once (ownership moves out of the frame, manually managed below); - // the right runs lazily in its own branch. Pass-through shape: the - // result IS the left box. Narrowed shape: the single non-unit arm's - // payload is extracted unionNarrow-style (+1 for ref kinds, the - // checker proved the tag) and the left box is released. - if (e.left.type.kind === "jsval") { - // `a ?? b` on an island value: the engine's nullish test; the - // right runs lazily in its branch (already jsval-typed). - const l = emitter.emitExpr(e.left); - emitter.moveTemp(l); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (scr_jsval_is_nullish(${l.name})) {`); - emitter.indent++; - emitter.releaseValue(l.name, e.left.type); - emitter.emitBranchInto(name, e.right); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${name} = ${l.name};`); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - if (e.left.type.kind === "dyn") { - // `a ?? b` on a checked-dynamic left: the runtime kind decides - // (UNDEF/NULL take the default; a wrapped island value asks the - // engine); the right runs lazily in its branch (already dyn). - const l = emitter.emitExpr(e.left); - emitter.moveTemp(l); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (scr_dyn_is_nullish(${l.name})) {`); - emitter.indent++; - emitter.releaseValue(l.name, e.left.type); - emitter.emitBranchInto(name, e.right); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${name} = ${l.name};`); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: nullish left is not a union"); - const def = emitter.unionsById.get(e.left.type.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: nullish of unknown union ${e.left.type.unionId}`); - const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : [])); - if (unitTags.length === 0) throw new InternalCompilerError("emitter bug: nullish union lacks unit arms"); - const l = emitter.emitExpr(e.left); - emitter.moveTemp(l); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - const test = unitTags.map((t) => `${l.name}->tag == ${t}`).join(" || "); - emitter.line(`if (${test}) {`); - emitter.indent++; - emitter.releaseValue(l.name, e.left.type); - emitter.emitBranchInto(name, e.right); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - if (typeEquals(e.type, e.left.type)) { - emitter.line(`${name} = ${l.name};`); - } else { - const arm = e.type; - const read = - arm.kind === "f64" - ? `scr_union_get_f64(${l.name})` - : arm.kind === "bool" - ? `scr_union_get_bool(${l.name})` - : retainCallC(arm, `(${cType(arm).trim()})scr_union_peek(${l.name})`); - emitter.line(`${name} = ${read};`); - emitter.releaseValue(l.name, e.left.type); - } - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitStringExpr( - emitter: CEmitter, - e: ExprOf<"strConcat" | "strEq" | "strCmp" | "toString" | "strIntrinsic" | "regexLit" | "templateStrings" | "regexIntrinsic">, -): Temp { - switch (e.kind) { - case "strConcat": { - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - return emitter.newTemp(e.type, `scr_str_concat(${l.name}, ${r.name})`); - } - case "strEq": { - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - return emitter.newTemp(e.type, `${e.negated ? "!" : ""}scr_str_eq(${l.name}, ${r.name})`); - } - case "strCmp": { - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp"; - return emitter.newTemp(e.type, `${fn}(${l.name}, ${r.name}) ${e.op} 0`); - } - case "toString": { - const v = emitter.emitExpr(e.operand); - if (v.type.kind === "union") { - // The ARM value's ToString via the per-union interned helper - // (unit arms are interned literals, string arms retain the - // payload, f64/bool arms format). Box borrowed; result +1. - return emitter.fallibleTemp(e.type, `${emitter.unionToStrHelper(v.type.unionId)}(${v.name})`); - } - if (v.type.kind === "caught") { - // String(e) over the exception snapshot. Box borrowed; result +1. - return emitter.fallibleTemp(e.type, `scr_caught_to_string(${v.name})`); - } - if (v.type.kind === "dyn") { - // String(unknown): dispatch over the dyn kind (dynToStrHelper — - // Node's String() incl. arrays-join and "[object Object]"). - return emitter.fallibleTemp(e.type, `${emitter.dynToStrHelper()}(${v.name})`); - } - if (v.type.kind === "record") { - // String(record) / `${record}`: Object.prototype.toString's - // constant. The operand temp was emitted above (effects and RC - // ride the frame like any other statement temp); the result is - // the interned literal, retained like a strLit. - const sym = emitter.internLiteral("[object Object]"); - return emitter.newTemp(e.type, retainCallC(e.type, `(ScrStr *)&${sym}`)); - } - const fn = v.type.kind === "bool" ? "scr_bool_to_scrstr" : "scr_f64_to_scrstr"; - return emitter.newTemp(e.type, `${fn}(${v.name})`); - } - case "strIntrinsic": { - // Receiver and string arguments are owned temps in the current - // frame; every scr_str_* method BORROWS them (frame release at - // statement end still applies). String-returning methods hand back - // a +1 reference, which newTemp registers in the frame like any - // other owned string temp. Omitted optional args get the C-side - // defaults from docs/ir.md: indexOf position 0, slice start 0, - // slice end INFINITY (math.h is always included). - const r = emitter.emitExpr(e.receiver); - const args = e.args.map((a) => emitter.emitExpr(a)); - const method = e.method; - switch (method) { - case "length": - return emitter.newTemp(e.type, `scr_str_utf16_len(${r.name})`); - case "charCodeAt": - return emitter.newTemp(e.type, `scr_str_char_code_at(${r.name}, ${args[0]!.name})`); - case "charAt": - return emitter.newTemp(e.type, `scr_str_char_at(${r.name}, ${args[0]!.name})`); - case "indexOf": - return emitter.newTemp( - e.type, - `scr_str_index_of(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"})`, - ); - case "includes": - // The position form is indexOf's clamp exactly (the spec - // routes both through StringIndexOf): found ⇔ index != -1. - if (args[1]) { - return emitter.newTemp( - e.type, - `scr_str_index_of(${r.name}, ${args[0]!.name}, ${args[1].name}) != -1`, - ); - } - return emitter.newTemp(e.type, `scr_str_includes(${r.name}, ${args[0]!.name})`); - case "startsWith": - return emitter.newTemp(e.type, args[1] - ? `scr_str_starts_with_from(${r.name}, ${args[0]!.name}, ${args[1].name})` - : `scr_str_starts_with(${r.name}, ${args[0]!.name})`); - case "endsWith": - return emitter.newTemp(e.type, args[1] - ? `scr_str_ends_with_from(${r.name}, ${args[0]!.name}, ${args[1].name})` - : `scr_str_ends_with(${r.name}, ${args[0]!.name})`); - case "slice": - return emitter.newTemp( - e.type, - `scr_str_slice(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`, - ); - case "substring": - return emitter.newTemp( - e.type, - `scr_str_substring(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "INFINITY"})`, - ); - case "repeat": - return emitter.newTemp(e.type, `scr_str_repeat(${r.name}, ${args[0]!.name})`); - case "trim": - return emitter.newTemp(e.type, `scr_str_trim(${r.name})`); - case "trimStart": - return emitter.newTemp(e.type, `scr_str_trim_start(${r.name})`); - case "trimEnd": - return emitter.newTemp(e.type, `scr_str_trim_end(${r.name})`); - case "split": - return emitter.newTemp( - e.type, - `scr_str_split_limit(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - case "padStart": - return emitter.newTemp( - e.type, - `scr_str_pad_start(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - case "padEnd": - return emitter.newTemp( - e.type, - `scr_str_pad_end(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - // The lre-backed pair (scr_regex.c): the IR's presence flips the - // regex link switch, so the symbols always resolve. - case "toLowerCase": - return emitter.newTemp(e.type, `scr_str_to_lower(${r.name})`); - case "toUpperCase": - return emitter.newTemp(e.type, `scr_str_to_upper(${r.name})`); - // The well-formedness pair: no-ops over well-formed storage - // (constant true / retained identity; scr_string.c). - case "isWellFormed": - return emitter.newTemp(e.type, `scr_str_is_well_formed(${r.name})`); - case "toWellFormed": - return emitter.newTemp(e.type, `scr_str_to_well_formed(${r.name})`); - case "cpAt": - return emitter.newTemp(e.type, `scr_str_cp_at(${r.name}, ${args[0]!.name})`); - default: { - const _exhaustive: never = method; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - case "regexLit": { - // 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 are interned NOW so the literal table is - // complete when emit() assembles the file. - const key = `${e.flags}/${e.pattern}`; - let sym = emitter.regexInstances.get(key); - if (!sym) { - sym = `sc_re_${emitter.regexInstances.size}`; - emitter.regexInstances.set(key, sym); - emitter.internLiteral(e.pattern); - emitter.internLiteral(e.flags); - } - return emitter.newTemp(e.type, `scr_regex_retain(&${sym})`); - } - case "templateStrings": { - // One immortal static string array per template SITE (the key); - // the +1 retain is a no-op on immortals but keeps the owned-temps - // discipline uniform. Cooked strings intern NOW so the literal - // table is complete when emit() assembles the file. - let inst = emitter.templateStringsInstances.get(e.key); - if (!inst) { - inst = { sym: `sc_tsa_${emitter.templateStringsInstances.size}`, cooked: e.cooked }; - emitter.templateStringsInstances.set(e.key, inst); - for (const s of e.cooked) emitter.internLiteral(s); - } - return emitter.newTemp(e.type, `scr_arr_retain(&${inst.sym})`); - } - case "regexIntrinsic": { - // Receiver and args are borrowed frame temps; string/array results - // come back +1 and join the frame via newTemp. replaceAll and split - // may THROW (catchable) — fallibleTemp emits the pending check. - const r = emitter.emitExpr(e.receiver); - const args = e.args.map((a) => emitter.emitExpr(a)); - const method = e.method; - switch (method) { - case "test": - return emitter.newTemp(e.type, `scr_regex_test(${r.name}, ${args[0]!.name})`); - case "match": { - // +1 string[] or NULL from the runtime; the `string[] | null` - // union wraps type-directedly, the process.envGet convention. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: match result not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const arrTag = def ? def.arms.findIndex((a) => a.kind === "array") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (arrTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: match union lacks its arms"); - } - const m = emitter.newTemp(arrayOf(STRING), `scr_regex_match(${r.name}, ${args[0]!.name})`); - emitter.moveTemp(m); // moves into the union box when present; NULL otherwise - const present = `scr_union_new_ref(${arrTag}, ${m.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${m.name} ? ${present} : ${absent}`); - } - case "matchAll": - // Every match drained eagerly into a fresh +1 string[][]; - // throws Node's TypeError on a non-global regex (catchable — - // fallibleTemp's pending check). - return emitter.fallibleTemp(e.type, `scr_regex_match_all(${r.name}, ${args[0]!.name})`); - case "matchAllInto": - // matchAll's companion-index form: args[1] (a number[]) also - // receives each match's UTF-16 start index. - return emitter.fallibleTemp( - e.type, - `scr_regex_match_all_into(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - case "search": - // First-match index or -1 — a plain double; never throws. - return emitter.newTemp(e.type, `scr_regex_search(${r.name}, ${args[0]!.name})`); - case "source": - return emitter.newTemp(e.type, `scr_regex_source(${r.name})`); - case "flags": - return emitter.newTemp(e.type, `scr_regex_flags(${r.name})`); - case "toString": - return emitter.newTemp(e.type, `scr_regex_to_string(${r.name})`); - case "replace": - return emitter.newTemp( - e.type, - `scr_regex_replace(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - case "replaceAll": - return emitter.fallibleTemp( - e.type, - `scr_regex_replace_all(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - case "split": - return emitter.fallibleTemp( - e.type, - `scr_regex_split_limit(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - default: { - const _exhaustive: never = method; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitContainerExpr( - emitter: CEmitter, - e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">, -): Temp { - switch (e.kind) { - case "arrayLit": { - // Allocate, then push each element in order. Ownership of refcounted - // plain elements moves into the array (interned literals are - // immortal, so the array's eventual release of them is a no-op). - // SPREAD positions hold a same-typed source array (borrowed): its - // elements copy in — _get_ref returns +1 and _push_ref takes - // ownership, so the copy loop is RC-balanced; the length is - // snapshotted before the loop. - if (e.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayLit of non-array type"); - const elem = e.type.elem; - const arr = emitter.newTemp(e.type, emitter.arrNewC(elem, e.elems.length)); - const acc = elemAccess(elem); - const spreadSet = new Set(e.spreads ?? []); - e.elems.forEach((el, i) => { - const v = emitter.emitExpr(el); - if (spreadSet.has(i)) { - emitter.line(`scr_arr_push_spread(${arr.name}, ${v.name});`); - return; - } - if (acc === "ref") emitter.moveTemp(v); - emitter.line(`scr_arr_push_${acc}(${arr.name}, ${v.name});`); - }); - return arr; - } - case "arrayNewLen": { - // Mapper-less Array.from({ length: n }): a length-n array of - // ABSENT slots — the interned undefined arm for unions carrying - // one (immortal: pushing owes no retain), NULL for every other - // ref element kind (assign before reading — SEMANTICS.md 46). The - // `i <= n - 1` bound is ToLength for the lengths that terminate: - // fractions truncate, negative/NaN produce an empty array. - if (e.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayNewLen of non-array type"); - const elem = e.type.elem; - const n = emitter.emitExpr(e.length); - const arr = emitter.newTemp(e.type, emitter.arrNewC(elem, 0)); - let fill = "NULL"; - if (elem.kind === "union") { - const def = emitter.unionsById.get(elem.unionId); - const tag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; - if (tag >= 0) fill = emitter.unitInstanceRef(elem.unionId, tag); - } - const i = `sc_i${emitter.tempCounter++}`; - emitter.line(`for (double ${i} = 0; ${i} <= ${n.name} - 1; ${i} += 1) {`); - emitter.indent++; - emitter.line(`scr_arr_push_${elemAccess(elem)}(${arr.name}, ${fill});`); - emitter.indent--; - emitter.line(`}`); - return arr; - } - case "arrayGet": { - const arr = emitter.emitExpr(e.arr); - const idx = emitter.emitExpr(e.index); - if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayGet on non-array"); - // Ref-element reads return +1 (the runtime retains); newTemp - // registers the owned temp in the frame like any other. - const acc = elemAccess(e.arr.type.elem); - return emitter.newTemp(e.type, `scr_arr_get_${acc}(${arr.name}, ${idx.name})`); - } - case "arrayHas": { - const arr = emitter.emitExpr(e.arr); - const idx = emitter.emitExpr(e.index); - if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayHas on non-array"); - return emitter.newTemp(e.type, `scr_arr_has(${arr.name}, ${idx.name})`); - } - case "arrayState": { - const arr = emitter.emitExpr(e.arr); - const idx = emitter.emitExpr(e.index); - if (e.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrayState on non-array"); - return emitter.newTemp(e.type, `scr_arr_state(${arr.name}, ${idx.name})`); - } - case "arrIntrinsic": { - // getNumber/indexEq produce scalars without invoking user code. A - // stable binding can own its receiver until that lookup finishes. - const r = e.method === "getNumber" || e.method === "indexEq" - ? emitStableReceiver(emitter, e.receiver, e.args) - : emitter.emitExpr(e.receiver); - if (e.receiver.type.kind !== "array") throw new InternalCompilerError("emitter bug: arrIntrinsic on non-array"); - const acc = elemAccess(e.receiver.type.elem); - const method = e.method; - switch (method) { - case "length": - return emitter.newTemp(e.type, `scr_arr_len(${r.name})`); - case "getNumber": { - const index = emitter.emitExpr(e.args[0]!); - const table = e.receiver.kind === "varRef" ? emitter.constantNumericTables.get(e.receiver.localId) : undefined; - return emitter.newTemp(e.type, `${table ? `${table.symbol}_get` : "scr_arr_get_number"}(${r.name}, ${index.name})`); - } - case "nextPresent": { - const start = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_next_present(${r.name}, ${start.name})`); - } - case "indexEq": { - const index = emitter.emitExpr(e.args[0]!); - const other = emitStableReceiver(emitter, e.args[1]!, [e.args[2]!]); - const otherIndex = emitter.emitExpr(e.args[2]!); - return emitter.newTemp(e.type, `scr_arr_index_eq(${r.name}, ${index.name}, ${other.name}, ${otherIndex.name})`); - } - case "push": { - // Variadic like JS: every argument evaluates first (left to - // right — an argument reading the array sees the pre-push - // state), then each appends in order. Ownership of refcounted - // arguments moves into the array; the result is the new length - // (f64) — the last push's return, or the unchanged length for - // Node's no-op zero-argument call. - const vs = e.args.map((a) => emitter.emitExpr(a)); - if (acc === "ref") vs.forEach((v) => emitter.moveTemp(v)); - for (let i = 0; i < vs.length - 1; i++) { - emitter.line(`scr_arr_push_${acc}(${r.name}, ${vs[i]!.name});`); - } - const last = vs[vs.length - 1]; - return last - ? emitter.newTemp(e.type, `scr_arr_push_${acc}(${r.name}, ${last.name})`) - : emitter.newTemp(e.type, `scr_arr_len(${r.name})`); - } - case "pushSpread": { - // `a.push(...src)`: append src's indexed state in order while - // preserving holes; the runtime snapshots self-spread and - // retains copied refs. Result: the new length. - const src = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_push_spread(${r.name}, ${src.name})`); - } - case "concatSpread": { - const src = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_concat_copy(${r.name}, ${src.name})`); - } - case "unshift": { - // Like push, every argument evaluates before mutation. Apply - // them from right to left so their final front order remains - // the source order. Ref ownership moves into the array. - const vs = e.args.map((a) => emitter.emitExpr(a)); - if (acc === "ref") vs.forEach((v) => emitter.moveTemp(v)); - let last: Temp | undefined; - for (let i = vs.length - 1; i >= 0; i--) { - last = emitter.newTemp(e.type, `scr_arr_unshift_${acc}(${r.name}, ${vs[i]!.name})`); - } - return last ?? emitter.newTemp(e.type, `scr_arr_len(${r.name})`); - } - case "unshiftSpread": { - // The runtime snapshots and retains the borrowed source, - // including the aliasing `a.unshift(...a)` case. - const src = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_unshift_spread(${r.name}, ${src.name})`); - } - case "pop": - case "shift": { - // Move the slot once, preserving undefined and holes as values - // while still applying the method's length/index mutation. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: array removal result is not a union"); - const elemT = e.receiver.type.elem; - const def = emitter.unionsById.get(e.type.unionId); - const tag = def ? def.arms.findIndex((arm) => typeEquals(arm, elemT)) : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - const sameUnion = elemT.kind === "union" && typeEquals(elemT, e.type); - if ((!sameUnion && tag < 0) || undefTag < 0) throw new InternalCompilerError("emitter bug: array removal union lacks its arms"); - const state = `sc_remove_state${emitter.tempCounter++}`; - const slot = `sc_remove_slot${emitter.tempCounter++}`; - emitter.line(`uint64_t ${slot} = 0;`); - emitter.line(`uint8_t ${state} = scr_arr_${method}_state(${r.name}, &${slot});`); - let present: string; - if (elemT.kind === "f64") { - const number = `sc_remove_number${emitter.tempCounter++}`; - emitter.line(`double ${number}; memcpy(&${number}, &${slot}, sizeof ${number});`); - present = `scr_union_new_f64(${tag}, ${number})`; - } else if (elemT.kind === "bool") { - present = `scr_union_new_bool(${tag}, ${slot} != 0)`; - } else { - const ref = `sc_remove_ref${emitter.tempCounter++}`; - emitter.line(`void *${ref}; memcpy(&${ref}, &${slot}, sizeof ${ref});`); - const rc = vAdapters(elemT); - present = sameUnion ? `(ScrUnion *)${ref}` : `scr_union_new_ref(${tag}, ${ref}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(elemT)})`; - } - return emitter.newTemp(e.type, `${state} == SCR_ARR_VALUE ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`); - } - case "indexOf": { - // The needle is BORROWED (released with this statement's frame); - // the ref variant dispatches on the array's element kind - // (strings by content, arrays by pointer). Strict equality. - const v = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_index_of_${acc}(${r.name}, ${v.name})`); - } - case "includes": { - // Borrowed needle, SameValueZero (NaN matches NaN). - const v = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_includes_${acc}(${r.name}, ${v.name})`); - } - case "join": { - // Separator borrowed; the result is an owned (+1) string. - // Union elements route through the per-union join walker - // (nullish arms print empty, exactly Array.prototype.join). - const sep = emitter.emitExpr(e.args[0]!); - if (e.receiver.type.kind === "array" && e.receiver.type.elem.kind === "union") { - return emitter.newTemp( - e.type, - `${emitter.unionJoinHelper(e.receiver.type.elem.unionId)}(${r.name}, ${sep.name})`, - ); - } - return emitter.newTemp(e.type, `scr_arr_join(${r.name}, ${sep.name})`); - } - case "slice": { - // Receiver borrowed; the result is a fresh +1 shallow copy - // (ref elements retained). Omitted indices get the JS - // defaults, the string-slice convention. - const start = e.args[0] ? emitter.emitExpr(e.args[0]).name : "0"; - const end = e.args[1] ? emitter.emitExpr(e.args[1]).name : "INFINITY"; - return emitter.newTemp(e.type, `scr_arr_slice(${r.name}, ${start}, ${end})`); - } - case "toReversed": - return emitter.newTemp(e.type, `scr_arr_to_reversed(${r.name})`); - case "reverse": - // Mutates in place and returns a retained reference to the - // receiver, preserving Array.prototype.reverse identity. - return emitter.newTemp(e.type, `scr_arr_reverse(${r.name})`); - case "toSpliced": { - const start = emitter.emitExpr(e.args[0]!); - const count = emitter.emitExpr(e.args[1]!); - const items = emitter.emitExpr(e.args[2]!); - return emitter.newTemp( - e.type, - `scr_arr_to_spliced(${r.name}, ${start.name}, ${count.name}, ${items.name})`, - ); - } - case "with": { - const index = emitter.emitExpr(e.args[0]!); - const value = emitter.emitExpr(e.args[1]!); - const out = emitter.newTemp( - e.type, - `scr_arr_with_${acc}(${r.name}, ${index.name}, ${value.name})`, - ); - emitter.emitPendingCheck(); - return out; - } - case "withUndefined": { - const index = emitter.emitExpr(e.args[0]!); - const out = emitter.newTemp( - e.type, - `scr_arr_with_undefined(${r.name}, ${index.name})`, - ); - emitter.emitPendingCheck(); - return out; - } - case "splice": { - // The removal splice: the removed elements come back as a - // fresh +1 array, their ownership MOVED out of the receiver - // (no retain/release churn). An omitted count removes to the - // end (+Infinity, the slice convention). - const start = emitter.emitExpr(e.args[0]!); - const cnt = e.args[1] ? emitter.emitExpr(e.args[1]).name : "INFINITY"; - return emitter.newTemp(e.type, `scr_arr_splice(${r.name}, ${start.name}, ${cnt})`); - } - case "spliceInsert": { - const start = emitter.emitExpr(e.args[0]!); - const count = emitter.emitExpr(e.args[1]!); - const items = emitter.emitExpr(e.args[2]!); - return emitter.newTemp(e.type, `scr_arr_splice_insert(${r.name}, ${start.name}, ${count.name}, ${items.name})`); - } - case "flatCopy": - case "flatOne": { - const out = emitter.emitExpr(e.args[0]!); - return emitter.newTemp(e.type, `scr_arr_flat_copy(${r.name}, ${out.name}, ${method === "flatOne" ? "true" : "false"})`); - } - default: { - const _exhaustive: never = method; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - case "bytesNew": { - // Typed-array/Buffer construction; the SOURCE's static type picks - // the runtime entry (see the node doc). The source is borrowed; - // every form hands back +1. Length and checked-input forms can - // throw; check after the result joins its ownership frame. - if (e.type.kind !== "bytes") throw new InternalCompilerError("emitter bug: bytesNew of non-bytes type"); - const kind = bytesElemKindC(e.type.elem); - if (!e.source) return emitter.newTemp(e.type, `scr_bytes_new(${kind}, 0)`); - const src = emitter.emitExpr(e.source); - if (e.source.type.kind === "f64") { - const t = emitter.newTemp(e.type, `scr_bytes_new(${kind}, ${src.name})`); - emitter.emitPendingCheck(); - return t; - } - if (e.source.type.kind === "bytes") { - return emitter.newTemp(e.type, `scr_bytes_convert(${kind}, ${src.name})`); - } - if (e.source.type.kind === "dyn") { - const t = emitter.newTemp(e.type, `scr_bytes_from_dyn(${kind}, ${src.name}, ${e.from ? "true" : "false"})`); - emitter.emitPendingCheck(); - return t; - } - if (e.source.type.kind === "array") { - return emitter.newTemp(e.type, `scr_bytes_from_arr(${kind}, ${src.name})`); - } - throw new InternalCompilerError(`emitter bug: bytesNew source of kind ${e.source.type.kind}`); - } - case "bytesIntrinsic": { - // Receiver and args are borrowed frame temps; string/bytes results - // come back +1. The MAY_THROW methods get the standard pending - // check after their temp joins its frame. - // - // The numeric families first: their kind token (args[0], always a - // strLit — validated) maps to the runtime tag at COMPILE time and - // never emits as a value; the remaining args are plain f64s. - if (e.method === "readNum" || e.method === "writeNum" || e.method === "readNumVar" || e.method === "writeNumVar") { - const tok = e.args[0]!; - if (tok.kind !== "strLit") throw new InternalCompilerError(`emitter bug: bytesIntrinsic ${e.method} kind must be a strLit`); - const r0 = emitter.emitExpr(e.receiver); - const rest = e.args.slice(1).map((a) => emitter.emitExpr(a)); - let call: string; - if (e.method === "readNum" || e.method === "writeNum") { - const spec = BYTES_NUM_KIND_C[tok.value]; - if (!spec) throw new InternalCompilerError(`emitter bug: bytes numeric kind '${tok.value}'`); - call = - e.method === "readNum" - ? `scr_bytes_read_num(${r0.name}, ${rest[0]!.name}, ${spec.kind}, ${spec.le})` - : `scr_bytes_write_num(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${spec.kind}, ${spec.le})`; - } else { - const spec = BYTES_NUM_VAR_C[tok.value]; - if (!spec) throw new InternalCompilerError(`emitter bug: bytes variable-width kind '${tok.value}'`); - call = - e.method === "readNumVar" - ? `scr_bytes_read_var(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${spec.sign}, ${spec.le})` - : `scr_bytes_write_var(${r0.name}, ${rest[0]!.name}, ${rest[1]!.name}, ${rest[2]!.name}, ${spec.sign}, ${spec.le})`; - } - const t = emitter.newTemp(e.type, call); - emitter.emitPendingCheck(); - return t; - } - const method = e.method; - const directElementAccess = method === "length" || method === "byteLength" || method === "get"; - const r = directElementAccess - ? emitStableReceiver(emitter, e.receiver, e.args) - : emitter.emitExpr(e.receiver); - const integerIndex = method === "get" ? emitter.integerLoopIndex(e.args[0]!) : null; - const args = integerIndex === null ? e.args.map((a) => emitter.emitExpr(a)) : []; - switch (method) { - case "length": - return emitter.newTemp(e.type, `(double)${r.name}->len`); - case "byteLength": - if (e.receiver.type.kind !== "bytes") { - throw new InternalCompilerError("emitter bug: bytesIntrinsic byteLength on non-bytes"); - } - return emitter.newTemp( - e.type, - `(double)(${r.name}->len * ${BYTES_ELEMENT_SIZE[e.receiver.type.elem]})`, - ); - case "get": - // Any invalid index traps (the array runtime's discipline). - if (e.receiver.type.kind !== "bytes") { - throw new InternalCompilerError("emitter bug: bytesIntrinsic get on non-bytes"); - } - return emitter.newTemp( - e.type, - `${emitter.bytesElementHelper("get", e.receiver.type.elem, integerIndex !== null)}(${r.name}, ${integerIndex ?? args[0]!.name})`, - ); - case "slice": - // Omitted relative indices default like string slice: start 0, - // end +Infinity (math.h is always included). Fresh copy, +1. - return emitter.newTemp( - e.type, - `scr_bytes_slice(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`, - ); - case "subarray": - // Same defaults; the result is a +1 VIEW aliasing the - // receiver's storage (subarray / Buffer's slice). - return emitter.newTemp( - e.type, - `scr_bytes_subarray(${r.name}, ${args[0]?.name ?? "0"}, ${args[1]?.name ?? "INFINITY"})`, - ); - case "toReversed": - return emitter.newTemp(e.type, `scr_bytes_to_reversed(${r.name})`); - case "copyWithin": - return emitter.newTemp(e.type, `scr_bytes_copy_within(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name})`); - case "with": { - const out = emitter.newTemp( - e.type, - `scr_bytes_with(${r.name}, ${args[0]!.name}, ${args[1]!.name})`, - ); - emitter.emitPendingCheck(); - return out; - } - case "join": - return emitter.newTemp( - e.type, - `scr_bytes_join(${r.name}, ${args[0]!.name})`, - ); - case "toArray": - return emitter.newTemp(e.type, `scr_bytes_to_arr(${r.name})`); - case "setFrom": - case "setFromDyn": { - // dst.set(src, offset?) — void; throws Node's RangeError on - // overflow (may-throw seed). - emitter.line( - `${method === "setFromDyn" ? "scr_bytes_set_from_dyn" : "scr_bytes_set_from"}(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"});${emitter.srcComment(e.loc)}`, - ); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - case "toString": - case "toStringVar": { - // The encoding arg is always present (the frontend completes - // an omitted one to "utf8"). The literal path is already - // canonical and cannot throw; toStringVar validates a runtime - // encoding and may throw ERR_UNKNOWN_ENCODING. Both return a - // +1 string. Range forms: [enc, start] decodes to the buffer's - // end; [enc, start, end] clamps the explicit end (negatives - // empty, Node's slice-then-decode). - const checked = method === "toStringVar"; - const stem = checked ? "scr_bytes_to_str_checked" : "scr_bytes_to_str"; - let out: Temp; - if (args.length === 3) { - out = emitter.newTemp(e.type, `${stem}_range(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name})`); - } else if (args.length === 2) { - out = emitter.newTemp(e.type, `${stem}_range(${r.name}, ${args[0]!.name}, ${args[1]!.name}, (double)${r.name}->len)`); - } else { - out = emitter.newTemp(e.type, `${stem}(${r.name}, ${args[0]!.name})`); - } - if (checked) emitter.emitPendingCheck(); - return out; - } - case "equals": - return emitter.newTemp(e.type, `scr_bytes_equals(${r.name}, ${args[0]!.name})`); - case "compareBuf": { - // nargs = the PRESENT index args (omitted ones skip Node's - // validation); the 0 placeholders are never read past nargs. - const n = e.args.length - 1; - const idx = [1, 2, 3, 4].map((i: number) => args[i]?.name ?? "0"); - const t = emitter.newTemp( - e.type, - `scr_bytes_compare(${r.name}, ${args[0]!.name}, ${n}, ${idx.join(", ")})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "indexOf": - case "lastIndexOf": - case "includes": { - // args = [needle, align, byteOffset?]; an omitted byteOffset - // is NaN — the runtime's search-everything default. - const fwd = method !== "lastIndexOf"; - const call = `scr_bytes_index_of(${r.name}, ${args[0]!.name}, ${args[2]?.name ?? "NAN"}, ${args[1]!.name}, ${fwd})`; - return emitter.newTemp(e.type, method === "includes" ? `(${call} != -1)` : call); - } - case "indexOfNum": - case "lastIndexOfNum": - case "includesNum": { - const fwd = method !== "lastIndexOfNum"; - const call = `scr_bytes_index_of_num(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "NAN"}, ${fwd})`; - return emitter.newTemp(e.type, method === "includesNum" ? `(${call} != -1)` : call); - } - case "fill": - case "fillNum": { - const fn = method === "fill" ? "scr_bytes_fill" : "scr_bytes_fill_num"; - const n = e.args.length - 1; - const t = emitter.newTemp( - e.type, - `${fn}(${r.name}, ${args[0]!.name}, ${n}, ${args[1]?.name ?? "0"}, ${args[2]?.name ?? "0"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "fillElem": - // Per-element TypedArray fill (non-u8): slice-style index - // defaults, never throws; the receiver comes back +1. - return emitter.newTemp( - e.type, - `scr_bytes_fill_elem(${r.name}, ${args[0]!.name}, ${args[1]?.name ?? "0"}, ${args[2]?.name ?? "INFINITY"})`, - ); - case "fillStr": { - const n = e.args.length - 2; - const t = emitter.newTemp( - e.type, - `scr_bytes_fill_str(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${n}, ` + - `${args[2]?.name ?? "0"}, ${args[3]?.name ?? "0"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "copy": { - const n = e.args.length - 1; - const t = emitter.newTemp( - e.type, - `scr_bytes_copy_into(${r.name}, ${args[0]!.name}, ${n}, ` + - `${args[1]?.name ?? "0"}, ${args[2]?.name ?? "0"}, ${args[3]?.name ?? "0"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "swap16": - case "swap32": - case "swap64": { - const w = method === "swap16" ? 2 : method === "swap32" ? 4 : 8; - const t = emitter.newTemp(e.type, `scr_bytes_swap(${r.name}, ${w})`); - emitter.emitPendingCheck(); - return t; - } - case "writeStr": { - const t = emitter.newTemp( - e.type, - `scr_bytes_write_str(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${args[2]!.name}, ` + - `${args[3]?.name ?? "0"}, ${args[3] ? "true" : "false"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "byteOffset": - // 0 for owners, the view's offset into its owner for a - // DataView. Never throws. - return emitter.newTemp(e.type, `scr_bytes_byte_offset(${r.name})`); - case "buffer": - return emitter.newTemp(e.type, `scr_array_buffer_from_bytes(${r.name})`); - case "dataViewNew": { - // new DataView(x.buffer, byteOffset?, byteLength?) — receiver - // is x itself (the frontend peeled `.buffer`). Omitted offset - // defaults to 0; the has_len flag keeps an omitted length - // distinct from every numeric value (the default is "to the - // end of the buffer"). Throws Node's RangeErrors; +1 view. - const t = emitter.newTemp( - e.type, - `scr_dataview_new(${r.name}, ${args[0]?.name ?? "0"}, ` + - `${args[1] ? "true" : "false"}, ${args[1]?.name ?? "0"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "dvGetUint8": - case "dvGetInt8": - case "dvGetUint16": - case "dvGetInt16": - case "dvGetUint32": - case "dvGetInt32": - case "dvGetFloat32": - case "dvGetFloat64": - case "dvGetBigUint64Number": - case "dvGetBigInt64Number": { - // DataView getters: an omitted littleEndian is big-endian - // (the JS default). Throw Node's constant RangeError on a bad - // offset. - const kind = DV_GET_KIND_C[method]; - const t = emitter.newTemp( - e.type, - `scr_dataview_get(${r.name}, ${args[0]!.name}, ${kind}, ${args[1]?.name ?? "false"})`, - ); - emitter.emitPendingCheck(); - return t; - } - case "dvSetUint8": - case "dvSetInt8": - case "dvSetUint16": - case "dvSetInt16": - case "dvSetUint32": - case "dvSetInt32": - case "dvSetFloat32": - case "dvSetFloat64": { - // DataView setters: [offset, value, littleEndian?] — void; - // throw the getters' constant RangeError on a bad offset. - const kind = DV_SET_KIND_C[method]; - emitter.line( - `scr_dataview_set(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${kind}, ` + - `${args[2]?.name ?? "false"});${emitter.srcComment(e.loc)}`, - ); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - default: { - const _exhaustive: never = method; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - case "mapNew": { - // Each reference side supplies its own RC adapters. Either trace - // adapter requires a collector header: keys as well as values can - // hold a path back to the map. - if (e.type.kind !== "map") throw new InternalCompilerError("emitter bug: mapNew of non-map type"); - const value = e.type.value; - const rc = isRefCounted(value) ? vAdapters(value) : null; - const keyRc = mapKeyAccess(e.type.key) === "ref" ? vAdapters(e.type.key) : null; - const args = `${mapKeyKindC(e.type.key)}, ${mapValKindC(value)}, `; - const valueArgs = `${rc ? `&${rc.retain}` : "NULL"}, ${rc ? `&${rc.release}` : "NULL"}, ${emitter.traceArgC(value)}`; - const m = emitter.newTemp( - e.type, - keyRc - ? `scr_map_new_typed(${args}&${keyRc.retain}, &${keyRc.release}, ${emitter.traceArgC(e.type.key)}, ${valueArgs})` - : `scr_map_new(${args}${valueArgs})`, - ); - // Seeded construction: set() each pair in source order — exactly - // the statements the user would write on an empty map, so a - // repeated key overwrites (the runtime releases the old value). - for (const pair of e.seed ?? []) { - const k = emitter.emitExpr(pair.key); - const v = emitter.emitExpr(pair.value); - if (elemAccess(value) === "ref") emitter.moveTemp(v); // value MOVES in, like mapIntrinsic set - emitter.line( - `scr_map_set_${mapKeyAccess(e.type.key)}_${elemAccess(value)}(${m.name}, ${k.name}, ${v.name});${emitter.srcComment(e.loc)}`, - ); - } - return m; - } - case "mapIntrinsic": - case "setIntrinsic": - return emitMapLikeIntrinsic(emitter, e); - case "setNew": { - // Empty set: the map runtime with the element as the KEY and the - // value slot pinned to the scalar kind (every stored value is 0.0, - // never read back). Identity elements supply key RC/trace adapters; - // scalar and string elements use the lean constructor. - if (e.type.kind !== "set") throw new InternalCompilerError("emitter bug: setNew of non-set type"); - // Reference elements (identity hashing) carry their RC adapters - // at construction — the scr_arr_new_ref technique. - const elemAcc = mapKeyAccess(e.type.elem); - const rcAdapters = elemAcc === "ref" ? vAdapters(e.type.elem) : null; - const s = emitter.newTemp( - e.type, - rcAdapters - ? `scr_map_new_typed(${mapKeyKindC(e.type.elem)}, SCR_MAP_VAL_F64, &${rcAdapters.retain}, &${rcAdapters.release}, ${emitter.traceArgC(e.type.elem)}, NULL, NULL, NULL)` - : `scr_map_new(${mapKeyKindC(e.type.elem)}, SCR_MAP_VAL_F64, NULL, NULL, NULL)`, - ); - // Seeded construction (`new Set(values)`): one borrowed T[] whose - // elements add() in order — the runtime helper walks the array - // (duplicates overwrite in place, insertion position preserved). - if (e.seed) { - const arr = emitter.emitExpr(e.seed); - emitter.line(`scr_set_add_all(${s.name}, ${arr.name});${emitter.srcComment(e.loc)}`); - } - return s; - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitCallExpr( - emitter: CEmitter, - e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">, -): Temp { - switch (e.kind) { - case "call": { - const args = e.args.map((a) => emitter.emitExpr(a)); - // Callees own their params: ownership of refcounted args moves. - for (const a of args) emitter.moveTemp(a); - // Async callee: the spawn wrapper runs the body eagerly to its - // first suspension and returns the promise (+1). The call itself - // never unwinds — rejections surface at await. - const call = `${emitter.callTargetC(e.callee)}(${args.map((a) => a.name).join(", ")})`; - if (e.type.kind === "void") { - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - if (emitter.mayThrow.has(e.callee)) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - const t = emitter.newTemp(e.type, call); - // The check runs AFTER the result temp joins the frame: an unwind - // releases it (the dummy is NULL for refcounted returns). - if (emitter.mayThrow.has(e.callee)) emitter.emitPendingCheck(); - return t; - } - case "ffiCall": { - // LIBRARY mode: every ffiCall is a profile-declared host-callback - // channel (the library lane loads no native-FFI manifest). The - // dispatch fetches the slot's registered pointer — or delivers the - // channel's unregistered-call trap through the funnel (SC4025) — - // then brackets only the typed indirect call, opaque context first. - // The bracket makes callback-time ABI re-entry a deterministic - // SC4026 trap before an inner entry can mutate runtime state. - // Marshalling matches the native ffiCall's value classes exactly: - // buffers are borrowed (ptr, len) for the call's duration, the - // u8/u32/i32 plumbing classes ride JS's ToUint32/ToInt32, and a - // scalar return converts to f64 exactly. The host cannot raise a - // scriptc exception, so no pending check follows. - const libCb = emitter.mod.lib?.callbacks?.find((c) => c.name === e.import); - if (libCb !== undefined) { - const cbArgs = e.args.map((arg) => emitter.emitExpr(arg)); - const natTypes: string[] = ["void *"]; - const natArgs: string[] = []; - libCb.params.forEach((cls, i) => { - const arg = cbArgs[i]!; - const native = (): string => `sc_t${emitter.tempCounter++}`; - switch (cls) { - case "f64": { - const value = native(); - emitter.line(`double ${value} = ${arg.name};`); - natTypes.push("double"); - natArgs.push(value); - break; - } - case "bool": { - const value = native(); - emitter.line(`uint8_t ${value} = (uint8_t)(${arg.name} ? 1 : 0);`); - natTypes.push("uint8_t"); - natArgs.push(value); - break; - } - case "u8": { - const value = native(); - emitter.line(`uint8_t ${value} = (uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0);`); - natTypes.push("uint8_t"); - natArgs.push(value); - break; - } - case "u32": { - const value = native(); - emitter.line(`uint32_t ${value} = (uint32_t)scr_bit_ushr(${arg.name}, 0.0);`); - natTypes.push("uint32_t"); - natArgs.push(value); - break; - } - case "i32": { - const value = native(); - emitter.line(`int32_t ${value} = (int32_t)scr_bit_or(${arg.name}, 0.0);`); - natTypes.push("int32_t"); - natArgs.push(value); - break; - } - case "string": - case "bytes": { - const ptr = native(); - const len = native(); - emitter.line(`const uint8_t *${ptr} = (const uint8_t *)${arg.name}->data;`); - emitter.line(`size_t ${len} = ${arg.name}->len;`); - natTypes.push("const uint8_t *", "size_t"); - natArgs.push(ptr, len); - break; - } - } - }); - const retC = - libCb.returns === "void" ? "void" - : libCb.returns === "f64" ? "double" - : libCb.returns === "bool" || libCb.returns === "u8" ? "uint8_t" - : libCb.returns === "u32" ? "uint32_t" - : "int32_t"; - const trapLit = cStringLiteral(Buffer.from(libCb.unregisteredTrap, "utf8")); - // Materialize the pointer and context before the callback-active - // bracket. This keeps the existing SC4025 fetch path outside the - // bracket and avoids C argument evaluation-order ambiguity. - const fn = `sc_t${emitter.tempCounter++}`; - emitter.line(`${retC} (*${fn})(${natTypes.join(", ")}) = (${retC} (*)(${natTypes.join(", ")}))scr_library_cb_require(${libCb.slot}, ${trapLit});`); - const ctx = `sc_t${emitter.tempCounter++}`; - emitter.line(`void *${ctx} = scr_library_cb_ctx(${libCb.slot});`); - const call = `${fn}(${[ctx, ...natArgs].join(", ")})`; - switch (libCb.returns) { - case "void": - emitter.line(`scr_library_callback_begin();`); - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - emitter.line(`scr_library_callback_end();`); - return { name: "", type: e.type }; - case "f64": { - const raw = `sc_t${emitter.tempCounter++}`; - emitter.line(`scr_library_callback_begin();`); - emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`); - emitter.line(`scr_library_callback_end();`); - return emitter.newTemp(e.type, raw); - } - case "bool": - { - const raw = `sc_t${emitter.tempCounter++}`; - emitter.line(`scr_library_callback_begin();`); - emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`); - emitter.line(`scr_library_callback_end();`); - return emitter.newTemp(e.type, `(${raw} != 0)`); - } - default: { // u8/u32/i32 — exact widenings back to f64 - const raw = `sc_t${emitter.tempCounter++}`; - emitter.line(`scr_library_callback_begin();`); - emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`); - emitter.line(`scr_library_callback_end();`); - return emitter.newTemp(e.type, `(double)${raw}`); - } - } - } - const entry = emitter.ffiByName.get(e.import); - if (!entry) throw new InternalCompilerError(`emitter bug: unknown FFI import ${e.import}`); - if (entry.callbackOperation) { - const adapter = emitter.ffiCallbackAdapter(entry.callbackTarget ?? entry.name, "callback"); - if (entry.callbackOperation === "release") { - emitter.line(`if (${adapter.global}) scr_ffi_release(&${adapter.table}, ${adapter.global});`); - return { name: "", type: e.type }; - } - const callback = emitter.emitExpr(e.args[0]!); - emitter.line(`scr_ffi_retain_slot(&${adapter.table}, &${adapter.global}, ${callback.name});`); - emitter.line(`scr_ffi_commit_slot(&${adapter.table}, ${callback.name});`); - return emitter.newTemp(e.type, `scr_bigint_from_pointer((void *)&${adapter.symbol})`); - } - const args = e.args.map((arg) => emitter.emitExpr(arg)); - const sourceArgs = new Map(); - const callbackArgs = new Map(); - let sourceIndex = 0; - entry.params.forEach((param, abiIndex) => { - if (isFfiContextParam(param)) return; - const arg = args[sourceIndex++]!; - sourceArgs.set(abiIndex, arg); - if (isFfiCallbackParam(param)) callbackArgs.set(param.callback.id, arg); - if (isFfiReleaseParam(param)) callbackArgs.set(param.callback.release, arg); - }); - - const { registrations: retainedRegistrations, releases: retainedReleases } = - collectFfiRetainedOps(entry, callbackArgs, (binding, id) => emitter.ffiCallbackAdapter(binding, id)); - - // Pin before registration. Raw retained descriptors are native - // singletons: the incoming closure is pinned (and an EMPTY slot - // armed) before the native set call, but a replaced registration - // stays live and dispatching until the call returns — a native - // setter may flush the outgoing callback one last time mid-replace. - // scr_ffi_commit_slot below repoints the slot and retires the - // superseded pins after the call. - for (const registration of retainedRegistrations) { - if (registration.global !== null) { - emitter.line(`scr_ffi_retain_slot(&${registration.table}, &${registration.global}, ${registration.callback.name});`); - } else if (registration.foreign) { - emitter.line(`scr_ffi_retain_foreign(&${registration.table}, ${registration.callback.name});`); - } else { - emitter.line(`scr_ffi_retain(&${registration.table}, ${registration.callback.name});`); - } - } - // Validate releases BEFORE the native removal call runs: a bogus - // release traps without native code observing any side effect. The - // registration itself is unpinned only after the call returns. - for (const release of retainedReleases) { - emitter.line(`scr_ffi_require${release.foreign ? "_foreign" : ""}(&${release.table}, ${release.callback.name});`); - } - - // Raw C callback pointers carry no userdata. For the documented - // call-scoped/same-thread policy, lend each one a distinct TLS - // slot for the dynamic extent of this native call. Save/restore - // makes nested and reentrant calls stack correctly. - const rawContexts: { tls: string; previous: Temp }[] = []; - for (const param of entry.params) { - if (!isFfiCallbackParam(param)) continue; - const adapter = emitter.ffiCallbackAdapter(entry.name, param.callback.id); - if (adapter.tls === null) continue; - const callback = callbackArgs.get(param.callback.id)!; - const previous = emitter.newBorrowedTemp(callback.type, adapter.tls); - emitter.line(`${adapter.tls} = ${callback.name};`); - rawContexts.push({ tls: adapter.tls, previous }); - } - const nativeArgs: string[] = []; - entry.params.forEach((param, i) => { - if (isFfiCallbackParam(param)) { - const adapter = emitter.ffiCallbackAdapter(entry.name, param.callback.id); - nativeArgs.push(`&${adapter.symbol}`); - return; - } - if (isFfiReleaseParam(param)) { - const { binding, id } = parseFfiCallbackKey(param.callback.release); - const adapter = emitter.ffiCallbackAdapter(binding, id); - nativeArgs.push(`&${adapter.symbol}`); - return; - } - if (isFfiContextParam(param)) { - const callback = callbackArgs.get(param.context); - if (!callback) throw new InternalCompilerError(`emitter bug: FFI context '${param.context}' has no callback arg`); - nativeArgs.push(`(void *)${callback.name}`); - return; - } - const arg = sourceArgs.get(i)!; - switch (param) { - case "i64": - case "u64": - case "pointer": - nativeArgs.push(`scr_bigint_to_${param}(${arg.name})`); - break; - case "mutable-bytes": - nativeArgs.push(`(uint8_t *)${arg.name}->data`, `${arg.name}->len`); - break; - case "f64": - nativeArgs.push(arg.name); - break; - case "f32": - nativeArgs.push(`(float)${arg.name}`); - break; - case "i8": - case "i16": { - const bits = param === "i8" ? 8 : 16; - const coerced = `sc_t${emitter.tempCounter++}`; - emitter.line(`uint32_t ${coerced} = (uint32_t)scr_bit_ushr(${arg.name}, 0.0) & ${(2 ** bits) - 1}u;`); - nativeArgs.push(`(int${bits}_t)(${coerced} < ${2 ** (bits - 1)}u ? (int32_t)${coerced} : (int32_t)${coerced} - ${2 ** bits})`); - break; - } - case "u16": - nativeArgs.push(`(uint16_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`); - break; - case "bool": - nativeArgs.push(`(uint8_t)(${arg.name} ? 1 : 0)`); - break; - case "u8": - nativeArgs.push(`(uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`); - break; - case "u32": - nativeArgs.push(`(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`); - break; - case "i32": - nativeArgs.push(`(int32_t)scr_bit_or(${arg.name}, 0.0)`); - break; - case "string": - nativeArgs.push(`(const uint8_t *)${arg.name}->data`, `${arg.name}->len`); - break; - case "bytes": - nativeArgs.push(`(const uint8_t *)${arg.name}->data`, `${arg.name}->len`); - break; - } - }); - const call = `${entry.symbol}(${nativeArgs.join(", ")})`; - const restoreRawContexts = (): void => { - for (let i = rawContexts.length - 1; i >= 0; i--) { - const saved = rawContexts[i]!; - emitter.line(`${saved.tls} = ${saved.previous.name};`); - } - }; - const finishRetainedReleases = (): void => { - // Commit raw replacements first (repoint the slot, retire the - // superseded pins), then unpin explicit releases — the runtime - // disarms the slot itself when the released closure holds it. - for (const registration of retainedRegistrations) { - if (registration.global !== null) { - emitter.line(`scr_ffi_commit_slot(&${registration.table}, ${registration.callback.name});`); - } - } - for (const release of retainedReleases) { - emitter.line(`scr_ffi_release${release.foreign ? "_foreign" : ""}(&${release.table}, ${release.callback.name});`); - } - }; - const callbacksMayThrow = callbackArgs.size > 0 || emitter.ffiHasRetainedCallback; - switch (entry.returns) { - case "i64": - case "u64": - case "pointer": { - const result = emitter.newTemp(e.type, `scr_bigint_from_${entry.returns}(${call})`); - restoreRawContexts(); - finishRetainedReleases(); - if (callbacksMayThrow) emitter.emitPendingCheck(); - return result; - } - case "void": - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - restoreRawContexts(); - finishRetainedReleases(); - if (callbacksMayThrow) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - case "f64": { - const result = emitter.newTemp(e.type, call); - restoreRawContexts(); - finishRetainedReleases(); - if (callbacksMayThrow) emitter.emitPendingCheck(); - return result; - } - case "bool": { - const result = emitter.newTemp(e.type, `(${call} != 0)`); - restoreRawContexts(); - finishRetainedReleases(); - if (callbacksMayThrow) emitter.emitPendingCheck(); - return result; - } - case "u8": - case "f32": - case "i8": - case "u16": - case "i16": - case "u32": - case "i32": { - const result = emitter.newTemp(e.type, `(double)${call}`); - restoreRawContexts(); - finishRetainedReleases(); - if (callbacksMayThrow) emitter.emitPendingCheck(); - return result; - } - } - } - case "closure": { - const target = emitter.fnByName.get(e.fnName); - if (!target) throw new InternalCompilerError(`emitter bug: closure over unknown function ${e.fnName}`); - if (target.captures === undefined) { - // Declared function as a value: the interned immortal closure — - // every mention yields the same pointer, so `f === f` is true. - emitter.fnValues.add(e.fnName); - return emitter.newTemp( - e.type, - retainCallC(e.type, `(ScrClosure *)&${mangleFnClosure(e.fnName)}`), - ); - } - const t = emitter.newTemp( - e.type, - `scr_closure_new((void *)&${emitter.callTargetC(e.fnName)}, ${e.captures.length})`, - ); - e.captures.forEach((localId, i) => { - emitter.line(`${t.name}->caps[${i}] = scr_box_retain(${mangleLocal(localId)});`); - }); - return t; - } - case "callValue": { - const callee = emitter.emitExpr(e.callee); - const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver); - const args = e.args.map((a) => emitter.emitExpr(a)); - for (const a of args) emitter.moveTemp(a); // callee owns its params - if (e.callee.type.kind !== "func") throw new InternalCompilerError("emitter bug: callValue on non-func"); - const cast = cFnPtrCast(e.callee.type); - const argList = [callee.name, ...args.map((a) => a.name)].join(", "); - const call = `(${cast}${callee.name}->fn)(${argList})`; - emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`); - if (e.type.kind === "void") { - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - emitter.line("scr_dyn_this_pop();"); - if (emitter.indirectMayThrow) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - const t = emitter.newTemp(e.type, call); - emitter.line("scr_dyn_this_pop();"); - if (emitter.indirectMayThrow) emitter.emitPendingCheck(); - return t; - } - case "selfRef": - // The running closure itself (env is borrowed; the result is owned). - return emitter.newTemp(e.type, retainCallC(e.type, "sc_env")); - case "new": { - // Allocate (fields zeroed), then run the ctor. The ctor owns and - // releases its `this` param like any callee, so it receives a +1 - // distinct from the one this expression returns. - const t = emitter.newTemp(e.type, `${mangleClassNew(e.className)}()`); - const args = e.args.map((a) => emitter.emitExpr(a)); - for (const a of args) emitter.moveTemp(a); - const ctorArgs = [`${mangleClassRetain(e.className)}(${t.name})`, ...args.map((a) => a.name)]; - emitter.line(`${mangleFunction(`%${e.className}.constructor`)}(${ctorArgs.join(", ")});${emitter.srcComment(e.loc)}`); - // A throwing constructor unwinds like any call; the half-built - // object is in this frame and releases with it. - if (emitter.mayThrow.has(`%${e.className}.constructor`)) emitter.emitPendingCheck(); - return t; - } - case "classRef": { - // The class itself as a value: the immortal class object's - // address. The +1 retain is a no-op on immortals but keeps the - // owned-temps discipline uniform (the regexLit pattern). - const sym = emitter.classObjSym(e.className); - if (e.captures !== undefined) { - const value = emitter.newTemp(e.type, `scr_classobj_new(&${sym}, ${e.captures.length})`); - e.captures.forEach((id, slot) => { - emitter.line(`${value.name}->caps[${slot}] = scr_box_retain(${mangleLocal(id)});`); - }); - return value; - } - return emitter.newTemp(e.type, `scr_classobj_retain(&${sym})`); - } - case "newValue": { - // Construction through a class VALUE: call the class object's - // construct thunk. Every value legally in the slot shares the - // static class's constructor ABI (the frontend's flow rule), so - // the cast spells exactly that signature; the thunk returns the - // +1 object as void * and the site reinterprets to the static - // class (a runtime descendant is the ordinary upcast story). - if (e.callee.type.kind !== "classval") { - throw new InternalCompilerError("emitter bug: newValue on non-classval callee"); - } - const cls = e.callee.type.className; - const ctor = emitter.fnByName.get(`%${cls}.constructor`); - if (!ctor) throw new InternalCompilerError(`emitter bug: newValue on ${cls} without a constructor`); - const callee = emitter.emitExpr(e.callee); - const args = e.args.map((a) => emitter.emitExpr(a)); - for (const a of args) emitter.moveTemp(a); // the constructor owns its params - const paramTypes = ctor.params.slice(1).map((p) => cType(p.type).trim()); - const cast = `(void *(*)(ScrClassObj *${paramTypes.length ? ", " + paramTypes.join(", ") : ""}))`; - const call = `(${cast}${callee.name}->ctor)(${[callee.name, ...args.map((a) => a.name)].join(", ")})`; - const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`); - if (newValueMayThrow(cls, emitter.classMeta.get(cls), emitter.mayThrow)) emitter.emitPendingCheck(); - return t; - } - case "instanceOfValue": { - // The interval check with the target loaded from the class object - // (same numbering the vtables carry). Frontend guarantees both - // sides are hierarchy members, so the operand has a vt word. - const v = emitter.emitExpr(e.value); - const target = emitter.emitExpr(e.classValue); - if (e.value.type.kind === "object" && emitter.classMeta.get(e.value.type.className)?.def.localCaptures !== undefined) { - return emitter.newTemp(e.type, `${v.name}->sc_class == ${target.name}`); - } - return emitter.newTemp( - e.type, - `${v.name}->vt->pre >= ${target.name}->pre && ${v.name}->vt->pre <= ${target.name}->post`, - ); - } - case "promiseVoidWiden": { - // One ScrPromise* either way — ownership transfers, type-only. - const v = emitter.emitExpr(e.value); - emitter.moveTemp(v); - return emitter.newTemp(e.type, v.name); - } - case "upcast": - case "downcast": { - // Prefix layout: both directions are pointer reinterprets of the - // SAME object — no RC traffic, ownership transfers from the - // operand temp to the result temp (the operand is struck so the - // one +1 releases exactly once). Classval upcasts are the same - // pointer with only the static type changing (one ScrClassObj - // struct covers every class), so the cast is a no-op spelling. - const v = emitter.emitExpr(e.value); - emitter.moveTemp(v); - return emitter.newTemp(e.type, `(${cType(e.type).trim()})${v.name}`); - } - case "instanceOf": { - // O(1) preorder-interval test against the vtable the object - // carries; the target's interval is a compile-time constant. - const v = emitter.emitExpr(e.value); - const target = emitter.classMeta.get(e.className); - if (!target) throw new InternalCompilerError(`emitter bug: instanceOf against unknown class ${e.className}`); - return emitter.newTemp( - e.type, - `${v.name}->vt->pre >= ${target.pre} && ${v.name}->vt->pre <= ${target.post}`, - ); - } - case "virtualCall": { - // Dispatch through the receiver's vtable: the slot lives on the - // method's root-most declaring class (an ancestor of the static - // class), whose signature the slot pointer carries — the receiver - // upcasts to it, another prefix-layout reinterpret. - const meta = emitter.classMeta.get(e.className); - if (!meta) throw new InternalCompilerError(`emitter bug: virtualCall on unknown class ${e.className}`); - const slot = meta.root.slots.find( - (sl) => - sl.method === e.method && sl.declarer.pre <= meta.pre && meta.pre <= sl.declarer.post, - ); - if (!slot) throw new InternalCompilerError(`emitter bug: no vtable slot for ${e.className}.${e.method}`); - const args = e.args.map((a) => emitter.emitExpr(a)); - for (const a of args) emitter.moveTemp(a); // callees own their params - const recv = args[0]!.name; - const recvArg = - slot.declarer === meta ? recv : `(${mangleClassStruct(slot.declarer.def.name)} *)${recv}`; - const vtt = mangleVtStruct(meta.root.def.name); - const argList = [recvArg, ...args.slice(1).map((a) => a.name)].join(", "); - const call = `((const ${vtt} *)${recv}->vt)->${slot.member}(${argList})`; - if (e.type.kind === "void") { - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - if (emitter.mayThrowMethods.has(e.method)) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - const t = emitter.newTemp(e.type, call); - if (emitter.mayThrowMethods.has(e.method)) emitter.emitPendingCheck(); - return t; - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitRecordExpr( - emitter: CEmitter, - e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">, -): Temp { - switch (e.kind) { - case "fieldGet": - case "recordGet": { - const obj = emitter.emitExpr(e.obj); - // Runtime error classes use ScrError's own member names. - const member = - e.kind === "fieldGet" && RUNTIME_ERROR_CLASSES.has(e.className) - ? e.field - : mangleField(e.field); - const field = `${obj.name}->${member}`; - return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, field) : field); - } - case "recordLit": { - // Allocate (fields zeroed), then write each field IN SOURCE ORDER — - // JS evaluates property values in source order. Ownership of - // refcounted values moves in; the struct is fresh, so there is - // never an old value to release. OVERFLOW entries (undeclared keys - // of an index-signature shape) insert into the shape's overflow - // map in the same interleaved order — the map takes ownership. - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: recordLit of non-record type"); - const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - for (const f of e.fields) { - if (f.drop) { - // A mapping-dropped field (the PromiseSettledResult subset): - // the initializer runs in its source-order slot — effects and - // throws included — and the result (if any) releases with the - // statement frame instead of storing. - emitter.emitExpr(f.value); - continue; - } - const v = emitter.emitExpr(f.value); - if (f.overflow) { - const lit = emitter.internLiteral(f.name); - const acc = - v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref"; - if (acc === "ref") emitter.moveTemp(v); - emitter.line( - `scr_map_set_str_${acc}(${rec.name}->${OVERFLOW_MEMBER}, (ScrStr *)&${lit}, ${v.name});`, - ); - continue; - } - if (isRefCounted(v.type)) emitter.moveTemp(v); - emitter.line(`${rec.name}->${mangleField(f.name)} = ${v.name};`); - } - return rec; - } - case "recordClone": { - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: recordClone of non-record type"); - emitter.recordCloneShapes.add(e.type.shapeId); - // Source first, then each override in source order. The helper - // returns a fully retained owned clone; replacement unlinks before - // releasing the copied value, matching recordSet's cycle discipline. - const source = emitter.emitExpr(e.source); - const rec = emitter.newTemp(e.type, `${mangleRecordClone(e.type.shapeId)}(${source.name})`); - for (const f of e.overrides) { - const v = emitter.emitExpr(f.value); - const field = `${rec.name}->${mangleField(f.name)}`; - if (isRefCounted(v.type)) { - emitter.moveTemp(v); - const old = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(v.type, old)} = ${field};`); - emitter.line(`${field} = ${v.name};`); - emitter.releaseValue(old, v.type); - } else { - emitter.line(`${field} = ${v.name};`); - } - } - return rec; - } - case "recordKeyGet": { - // Dynamic-keyed record read through the per-(shape, result type) - // helper — declared-field string switch, then the overflow map. - // Never throws (a smuggled miss traps in the helper). - const obj = emitter.emitExpr(e.obj); - const key = emitter.emitExpr(e.key); - const helper = emitter.recordKeyGetHelper(e.shapeId, e.type, e.overflowOnly === true); - return emitter.newTemp(e.type, `${helper}(${obj.name}, ${key.name})`); - } - case "recordOvfHas": { - const obj = emitter.emitExpr(e.obj); - const key = emitter.emitExpr(e.key); - return emitter.newTemp(e.type, `scr_map_has_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name})`); - } - case "recordOvfKeys": { - // The overflow map's live keys in JS own-key order — a fresh - // string[] snapshot (+1); the record is borrowed. - const obj = emitter.emitExpr(e.obj); - return emitter.newTemp(e.type, `scr_map_keys_js_order(${obj.name}->${OVERFLOW_MEMBER})`); - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitDynamicExpr( - emitter: CEmitter, - e: ExprOf<"dynFrom" | "dynFromJsval" | "dynCall" | "dynInvoke" | "dynArrLit" | "dynObjLit" | "unionWrap" | "unionNarrow" | "unionDisc" | "unionKeyGet" | "unionIsTag" | "dynKeyGet" | "dynHasKey" | "dynScalarEq" | "dynTest" | "unionEq" | "unionFuncEq" | "caughtTest" | "caughtCheck" | "caughtNarrow" | "caughtToDyn">, -): Temp { - switch (e.kind) { - case "dynFrom": { - // Static value → fresh dyn tree (+1) through the interned per-type - // converter; the operand stays borrowed (frame-released as usual). - // Bare unit literals (an `undefined`/`null` stored under an - // `unknown` index signature) are the dyn unit values directly. - if (e.value.kind === "unitLit") { - return emitter.newTemp( - e.type, - e.value.unit === "undefined" - ? "scr_dyn_retain(scr_dyn_undefined())" - : "scr_dyn_new_null()", - ); - } - const v = emitter.emitExpr(e.value); - const identityRef = - isDynTypedRefType(v.type) || - (v.type.kind === "union" && - (emitter.unionsById.get(v.type.unionId)?.arms.some(isDynTypedRefType) ?? false)); - if (e.liveRef || identityRef) { - if (v.type.kind === "union") { - const adapter = liveDynUnionRefAdapter(emitter, v.type); - return emitter.newTemp(e.type, `${adapter}(${v.name})`); - } - const adapter = liveDynRefAdapter(emitter, v.type); - const rc = vAdapters(v.type); - const key = typeKey(v.type); - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - return emitter.newTemp( - e.type, - `scr_dyn_new_typed_ref${emitter.traceAdapterC(v.type) !== null ? "_traced" : ""}(${v.name}, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit})`, - ); - } - if (v.type.kind === "func") { - // A closure boxes as the checked-dynamic tree's function kind: retained closure + - // the per-signature call thunk + the interned signature key. The - // best-effort name rides along for inspect/error rendering (NULL - // when the lowering had none). - const name = - e.fnName !== undefined && e.fnName !== "" - ? cStringLiteral(Buffer.from(e.fnName, "utf8")) - : "NULL"; - return emitter.newTemp(e.type, `${emitter.dynFuncBoxHelper(v.type)}(${v.name}, ${name})`); - } - return emitter.newTemp(e.type, `${emitter.toDynHelper(v.type)}(${v.name})`); - } - case "dynFromJsval": { - // Island value → dyn: the by-reference wrap (scr_dyn_from_jsval - // retains the cell in; engine scalars normalize to native dyn - // kinds at wrap time). Operand borrowed, result +1, never throws. - const v = emitter.emitExpr(e.value); - return emitter.newTemp(e.type, `scr_dyn_from_jsval(${v.name})`); - } - case "dynCall": { - // Calling a dyn value: args are already dyn (the lowering boxed or - // converted them); everything is BORROWED by scr_dyn_call — the - // boxed thunk builds its own typed copies — so the temps release - // with the frame as usual. The callee's source spelling rides - // along for Node's " is not a function" TypeError. - const callee = emitter.emitExpr(e.callee); - const receiver = e.receiver === undefined ? null : emitter.emitExpr(e.receiver); - const invoke = (call: string): Temp => { - emitter.line(`scr_dyn_this_push_dyn(${receiver?.name ?? "NULL"});`); - const result = emitter.newTemp(e.type, call); - emitter.line("scr_dyn_this_pop();"); - emitter.emitPendingCheck(); - return result; - }; - const what = cStringLiteral(Buffer.from(e.calleeName, "utf8")); - if (e.spreads !== undefined && e.spreads.length > 0) { - // The RUNTIME-ARITY form (`f(...args)`): one fresh dyn array - // collects the arguments left-to-right — plain args move in - // (push takes ownership), spread args FLATTEN (push_spread - // retains elements in and throws V8's spread-call TypeError for - // non-iterable dyn kinds, checked per spread — JS's - // ArgumentListEvaluation order) — then apply calls through the - // array's elements (borrowed, exactly scr_dyn_call). - const spreadAt = new Map(e.spreads.map((s) => [s.arg, s.what])); - const pack = emitter.newTemp(DYN, "scr_dyn_new_arr()"); - e.args.forEach((a, i) => { - const v = emitter.emitExpr(a); - const spreadWhat = spreadAt.get(i); - if (spreadWhat !== undefined) { - const w = cStringLiteral(Buffer.from(spreadWhat, "utf8")); - emitter.line(`scr_dyn_arr_push_spread(${pack.name}, ${v.name}, ${w});`); - emitter.emitPendingCheck(); - } else { - emitter.moveTemp(v); - emitter.line(`scr_dyn_arr_push(${pack.name}, ${v.name});`); - } - }); - return invoke(`scr_dyn_apply(${callee.name}, ${pack.name}, ${what})`); - } - const args = e.args.map((a) => emitter.emitExpr(a)); - let argsExpr = "NULL"; - if (args.length > 0) { - const arr = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrDyn *${arr}[${args.length}] = { ${args.map((a) => a.name).join(", ")} };`); - argsExpr = arr; - } - return invoke(`scr_dyn_call(${callee.name}, ${argsExpr}, ${args.length}, ${what})`); - } - case "dynInvoke": { - // Prototype-method dispatch on a dyn receiver: everything is - // BORROWED by scr_dyn_invoke (temps release with the frame); the - // result is owned and may ride a pending exception. - const recv = emitter.emitExpr(e.recv); - const args = e.args.map((a) => emitter.emitExpr(a)); - let argsExpr = "NULL"; - if (args.length > 0) { - const arr = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrDyn *${arr}[${args.length}] = { ${args.map((a) => a.name).join(", ")} };`); - argsExpr = arr; - } - const method = cStringLiteral(Buffer.from(e.method, "utf8")); - const what = cStringLiteral(Buffer.from(e.calleeName, "utf8")); - return emitter.fallibleTemp( - e.type, - `scr_dyn_invoke(${recv.name}, ${method}, ${argsExpr}, ${args.length}, ${what})`, - ); - } - case "dynArrLit": { - // A dyn array built element-by-element: ownership of each dyn - // element MOVES into the array (scr_dyn_arr_push's contract). - const arr = emitter.newTemp(e.type, "scr_dyn_new_arr()"); - for (const el of e.elems) { - const v = emitter.emitExpr(el); - emitter.moveTemp(v); - emitter.line(`scr_dyn_arr_push(${arr.name}, ${v.name});`); - } - return arr; - } - case "dynObjLit": { - // A dyn object built member-by-member: key then value, source - // order (JS's literal evaluation). scr_dyn_key_set BORROWS all - // three (the member retains the value in), so key/value temps - // release with the frame as usual; the receiver is a fresh OBJ, - // so the non-object throw paths are unreachable here. - const obj = emitter.newTemp(e.type, "scr_dyn_new_obj()"); - for (const f of e.fields ?? []) { - const k = emitter.emitExpr(f.key); - const v = emitter.emitExpr(f.value); - emitter.line(`scr_dyn_key_set(${obj.name}, ${k.name}, ${v.name});`); - } - return obj; - } - case "unionWrap": { - // Construct a fresh immutable tagged box. Ownership of a refcounted - // payload MOVES into the union (which stores the arm's _v RC entry - // points so a generic release can free it); scalars ride the slot. - // Unit arms (undefined/null) carry NO payload: every wrap yields - // THE interned immortal instance for this (union, tag) — no - // allocation, and the frame's release is a no-op (rc == SIZE_MAX, - // which the RC entry points and the cycle collector both skip). - const arm = e.value.type; - if (isUnitType(arm)) { - return emitter.newTemp(e.type, emitter.unitInstanceRef(e.unionId, e.tag)); - } - // A VOID payload (a void call wrapping into an undefined arm): - // evaluate for effects, produce the interned unit instance. - if (arm.kind === "void") { - emitter.emitExpr(e.value); - return emitter.newTemp(e.type, emitter.unitInstanceRef(e.unionId, e.tag)); - } - const v = emitter.emitExpr(e.value); - if (isRefCounted(arm)) { - emitter.moveTemp(v); - const rc = vAdapters(arm); - return emitter.newTemp( - e.type, - `scr_union_new_ref(${e.tag}, ${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arm)})`, - ); - } - if (arm.kind === "f64") return emitter.newTemp(e.type, `scr_union_new_f64(${e.tag}, ${v.name})`); - if (arm.kind === "bool") return emitter.newTemp(e.type, `scr_union_new_bool(${e.tag}, ${v.name})`); - throw new InternalCompilerError(`emitter bug: unionWrap of ${arm.kind}`); - } - case "unionNarrow": { - // Tag-UNCHECKED payload extraction: the frontend emits this only - // where tsc's control-flow narrowing proved the tag (docs/ir.md). - // Ref payloads come out +1 via the arm's CONCRETE retain (statically - // known here — no need for the stored fn ptr); the union temp itself - // releases with this statement's frame as usual. - const u = emitter.emitExpr(e.value); - const arm = e.type; - if (isUnitType(arm)) throw new InternalCompilerError(`emitter bug: unionNarrow to unit arm ${arm.kind}`); - if (arm.kind === "f64") return emitter.newTemp(arm, `scr_union_get_f64(${u.name})`); - if (arm.kind === "bool") return emitter.newTemp(arm, `scr_union_get_bool(${u.name})`); - const payload = `(${cType(arm).trim()})scr_union_peek(${u.name})`; - return emitter.newTemp(arm, retainCallC(arm, payload)); - } - case "unionDisc": { - // Shared-field read `r.kind` / `spec.config`: switch on the runtime - // tag and read the (same-typed) field from the concretely-cast - // payload. Ref-counted results come out retained (+1), owned by - // this frame. - const u = emitter.emitExpr(e.value); - const def = emitter.unionsById.get(e.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: unionDisc of unknown union ${e.unionId}`); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`switch (${u.name}->tag) {`); - emitter.indent++; - def.arms.forEach((arm, i) => { - if (arm.kind !== "record" && arm.kind !== "object") { - throw new InternalCompilerError(`emitter bug: unionDisc arm of kind ${arm.kind}`); - } - // cType names the arm's struct (incl. the runtime ScrError for - // builtin error arms, whose members are unmangled). - const member = - arm.kind === "object" && RUNTIME_ERROR_CLASSES.has(arm.className) - ? e.field - : mangleField(e.field); - const read = `((${cType(arm).trim()})scr_union_peek(${u.name}))->${member}`; - const value = isRefCounted(e.type) ? retainCallC(e.type, read) : read; - emitter.line(`case ${i}: ${name} = ${value}; break;`); - }); - emitter.line(`default: scr_trap("scriptc: internal error: invalid union tag\\n");`); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "unionKeyGet": { - // The unionDisc generalization: switch on the runtime tag; each arm - // answers at the JOIN type — a declared field reads its slot - // (wrapping an arm-typed answer into the join), an index-signature - // arm goes through the shared keyed-read helper (owned result, - // missing-key policy included), and a unit arm answers the interned - // undefined arm (the optional-chain tail's short-circuit value). - const u = emitter.emitExpr(e.value); - const k = emitter.emitExpr(e.key); - const def = emitter.unionsById.get(e.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: unionKeyGet of unknown union ${e.unionId}`); - const resultDef = e.type.kind === "union" ? emitter.unionsById.get(e.type.unionId) : undefined; - const literal = e.key.kind === "strLit" ? e.key.value : null; - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`switch (${u.name}->tag) {`); - emitter.indent++; - def.arms.forEach((arm, i) => { - if (isUnitType(arm)) { - const tag = resultDef?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1; - if (tag < 0 || e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: unionKeyGet unit arm without an undefined result arm"); - } - emitter.line(`case ${i}: ${name} = ${emitter.unitInstanceRef(e.type.unionId, tag)}; break;`); - return; - } - if (arm.kind === "array") { - // A NUMBER-keyed element read (the chain-tail `u?.split(":")[0]` - // form): the runtime getter answers owned (+1 for ref - // elements); invalid indices trap (divergence 4). The result - // wraps into the join when unit arms widened it. - const read = `scr_arr_get_${elemAccess(arm.elem)}((${cType(arm).trim()})scr_union_peek(${u.name}), ${k.name})`; - if (typeEquals(arm.elem, e.type)) { - emitter.line(`case ${i}: ${name} = ${read}; break;`); - return; - } - if (arm.elem.kind === "union" && e.type.kind === "union") { - const helper = unionWidenHelper(emitter, arm.elem.unionId, e.type.unionId); - emitter.line(`case ${i}: { ScrUnion *hit = ${read}; ${name} = ${helper}(hit); scr_union_release(hit); break; }`); - return; - } - const tag = resultDef?.arms.findIndex((a) => typeEquals(a, arm.elem)) ?? -1; - if (tag < 0 || e.type.kind !== "union" || isUnitType(arm.elem)) { - throw new InternalCompilerError(`emitter bug: unionKeyGet element ${arm.elem.kind} outside the join`); - } - const wrapped = - arm.elem.kind === "f64" - ? `scr_union_new_f64(${tag}, ${read})` - : arm.elem.kind === "bool" - ? `scr_union_new_bool(${tag}, ${read})` - : (() => { - const rc = vAdapters(arm.elem); - // The element read is already owned (+1) — ownership - // MOVES into the union box, no extra retain. - return `scr_union_new_ref(${tag}, ${read}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arm.elem)})`; - })(); - emitter.line(`case ${i}: ${name} = ${wrapped}; break;`); - return; - } - if (arm.kind !== "record") throw new InternalCompilerError(`emitter bug: unionKeyGet arm of kind ${arm.kind}`); - const shape = emitter.recordsById.get(arm.shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: unionKeyGet arm of unknown shape ${arm.shapeId}`); - const declared = literal !== null ? shape.fields.find((f) => f.name === literal) : undefined; - if (declared) { - const ft = declared.type; - const read = `((${cType(arm).trim()})scr_union_peek(${u.name}))->${mangleField(declared.name)}`; - if (typeEquals(ft, e.type)) { - emitter.line(`case ${i}: ${name} = ${isRefCounted(ft) ? retainCallC(ft, read) : read}; break;`); - return; - } - if (ft.kind === "union" && e.type.kind === "union") { - const helper = unionWidenHelper(emitter, ft.unionId, e.type.unionId); - emitter.line(`case ${i}: ${name} = ${helper}(${read}); break;`); - return; - } - const tag = resultDef?.arms.findIndex((a) => typeEquals(a, ft)) ?? -1; - if (tag < 0 || e.type.kind !== "union" || isUnitType(ft)) { - throw new InternalCompilerError(`emitter bug: unionKeyGet arm answer ${ft.kind} outside the join`); - } - const wrapped = - ft.kind === "f64" - ? `scr_union_new_f64(${tag}, ${read})` - : ft.kind === "bool" - ? `scr_union_new_bool(${tag}, ${read})` - : (() => { - const rc = vAdapters(ft); - return `scr_union_new_ref(${tag}, ${retainCallC(ft, read)}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(ft)})`; - })(); - emitter.line(`case ${i}: ${name} = ${wrapped}; break;`); - return; - } - // Index-signature arm (or declared-only shape under a runtime - // key): the per-(shape, join type) keyed-read helper — a literal - // key naming no declared field touches only the overflow map. - const helper = emitter.recordKeyGetHelper(arm.shapeId, e.type, literal !== null && !!shape.indexValue); - emitter.line(`case ${i}: ${name} = ${helper}((${cType(arm).trim()})scr_union_peek(${u.name}), ${k.name}); break;`); - }); - emitter.line(`default: scr_trap("scriptc: internal error: invalid union tag\\n");`); - emitter.indent--; - emitter.line(`}`); - if (isRefCounted(e.type)) emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - case "unionIsTag": { - // A pure tag compare — the box is borrowed, no payload is touched. - const u = emitter.emitExpr(e.value); - return emitter.newTemp(e.type, `${u.name}->tag ${e.negated ? "!=" : "=="} ${e.tag}`); - } - case "dynKeyGet": { - // Keyed read on the checked-dynamic tree through the one interned helper — the - // non-optional form throws JS's TypeError on an undefined/null - // receiver, and HANDLE receivers can throw the loud unmodeled- - // property ladder on EITHER form, so both ride a fallible temp; - // the result is owned (+1). - const d = emitter.emitExpr(e.value); - const k = emitter.emitExpr(e.key); - const helper = dynKeyGetHelper(emitter); - const call = `${helper}(${d.name}, ${k.name}, ${e.optional ? "true" : "false"})`; - return emitter.fallibleTemp(e.type, call); - } - case "dynHasKey": { - const value = emitter.emitExpr(e.value); - const key = emitter.emitExpr({ kind: "strLit", value: e.key, type: STRING, loc: e.loc }); - const test = `scr_dyn_has_key(${value.name}, ${key.name})`; - return emitter.fallibleTemp(e.type, e.negated ? `!${test}` : test); - } - case "dynScalarEq": { - // dyn vs scalar strict equality: kind test + payload compare. - // Operands emit in SOURCE order (JS evaluation order); the dyn - // side is found by type. Both borrowed, no allocation. - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - const [d, s, st] = e.left.type.kind === "dyn" ? [l, r, e.right.type] : [r, l, e.left.type]; - const test = - st.kind === "dyn" - ? // dyn vs dyn: whole-dyn strict equality (scalars by value, - // units by kind, reference kinds by node identity). - `scr_dyn_strict_eq(${l.name}, ${r.name})` - : st.kind === "string" - ? `(${d.name}->kind == SCR_DYN_STR && scr_str_eq(${d.name}->v.str, ${s.name}))` - : st.kind === "f64" - ? `(${d.name}->kind == SCR_DYN_NUM && ${d.name}->v.num == ${s.name})` - : `(${d.name}->kind == SCR_DYN_BOOL && ${d.name}->v.b == ${s.name})`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - case "dynTest": { - // A pure kind compare on the dyn node — borrowed; only the truthy - // form also reads a scalar payload. ISLAND-held nodes (the jsval - // kind — engine objects/arrays/functions only, scalars normalize - // at wrap time) route the tests that depend on the engine's - // answer through the scr_dyn_isl_* helpers (false on every other - // kind, so the calls stay unconditional); narrowing never changes - // representation (SEMANTICS.md). - const d = emitter.emitExpr(e.value); - if (e.test === "bytes") { - const test = `scr_dyn_bytes_is(${d.name}, ${bytesElemKindC(e.bytesElem ?? "u8")})`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - if (e.test === "buffer") { - const test = `(${d.name}->kind == SCR_DYN_BYTES && ${d.name}->buffer)`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - const test = - e.test === "nullish" - ? `(${d.name}->kind == SCR_DYN_UNDEF || ${d.name}->kind == SCR_DYN_NULL)` - : e.test === "object" - ? // `typeof v === "object"`: objects, arrays, bytes, native - // handles, promises, AND null — engine-held objects by the - // engine's own typeof. - `(${d.name}->kind == SCR_DYN_OBJ || ${d.name}->kind == SCR_DYN_ARR || ${d.name}->kind == SCR_DYN_BYTES || ${d.name}->kind == SCR_DYN_HANDLE || ${d.name}->kind == SCR_DYN_PROMISE || ${d.name}->kind == SCR_DYN_PROXY || ${d.name}->kind == SCR_DYN_NULL || scr_dyn_isl_typeof_is(${d.name}, "object"))` - : e.test === "truthy" - ? // Runtime ToBoolean includes typed-reference capsules and - // keeps this backend in lockstep with LLVM. - `scr_dyn_truthy(${d.name})` - : e.test === "error" - ? // `u instanceof Error`: the checked-dynamic tree's error encoding — an - // object carrying the reserved "%error" marker key - // (built by caughtToDyn for Error payloads) — or a real - // engine Error held by reference. - `((${d.name}->kind == SCR_DYN_OBJ && scr_dyn_obj_get(${d.name}, "%error", 6) != NULL) || scr_dyn_isl_is_error(${d.name}))` - : e.test === "array" - ? // Array.isArray: the checked-dynamic tree's array kind, or the engine's - // own answer for an engine-held value. - `(${d.name}->kind == SCR_DYN_ARR || scr_dyn_isl_is_array(${d.name}))` - : e.test === "function" - ? `(${d.name}->kind == SCR_DYN_FUNC || scr_dyn_isl_typeof_is(${d.name}, "function"))` - : `${d.name}->kind == ${ - { bigint: "SCR_DYN_BIGINT", string: "SCR_DYN_STR", number: "SCR_DYN_NUM", boolean: "SCR_DYN_BOOL", undefined: "SCR_DYN_UNDEF", null: "SCR_DYN_NULL", bytes: "SCR_DYN_BYTES" }[e.test] - }`; - return emitter.newTemp(e.type, e.negated ? `!(${test})` : test); - } - case "unionEq": { - // Strict equality (or Object.is's SameValue — the f64 arm's - // compare is the one difference) of the ARM values via the - // per-union helper (tag compare + per-arm payload compare). Both - // boxes are borrowed. - const l = emitter.emitExpr(e.left); - const r = emitter.emitExpr(e.right); - const call = `${emitter.unionEqHelper(e.unionId, e.sameValue)}(${l.name}, ${r.name})`; - return emitter.newTemp(e.type, e.negated ? `!${call}` : call); - } - case "unionFuncEq": { - const u = emitter.emitExpr(e.union); - const f = emitter.emitExpr(e.func); - const test = `(${u.name}->tag == ${e.tag} && scr_union_peek(${u.name}) == ${f.name})`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - case "caughtTest": { - // Kind-tag tests read the snapshot directly; instanceof compares an - // OBJ payload's vtable preorder against the class's compile-time - // interval (false for every other payload kind). Box borrowed. - const c = emitter.emitExpr(e.value); - if (e.test === "instanceof") { - const target = emitter.classMeta.get(e.className!); - if (!target) throw new InternalCompilerError(`emitter bug: caughtTest against unknown class ${e.className}`); - const test = `scr_caught_instanceof(${c.name}, ${target.pre}, ${target.post})`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - if (e.test === "object") { - const test = `scr_caught_is_object(${c.name})`; - return emitter.newTemp(e.type, e.negated ? `!${test}` : test); - } - const tag = { string: "SCR_EXC_STR", number: "SCR_EXC_F64", boolean: "SCR_EXC_BOOL" }[e.test]; - return emitter.newTemp(e.type, `${c.name}->kind ${e.negated ? "!=" : "=="} ${tag}`); - } - case "caughtCheck": { - // Checked payload extraction (`e as C`): instanceof match extracts - // +1, anything else throws the catchable TypeError — the result - // joins the frame BEFORE the pending check so an unwind releases - // the NULL dummy harmlessly. Box borrowed. - const c = emitter.emitExpr(e.value); - const target = emitter.classMeta.get(e.className); - if (!target) throw new InternalCompilerError(`emitter bug: caughtCheck against unknown class ${e.className}`); - const display = e.className.startsWith("%") ? e.className.slice(1) : e.className; - return emitter.fallibleTemp( - e.type, - `(${cType(e.type).trim()})scr_caught_check_obj(${c.name}, ${target.pre}, ${target.post}, ${cStringLiteral(Buffer.from(display, "utf8"))})`, - ); - } - case "caughtNarrow": { - // Checker-trusted extraction (the matching caughtTest was proven by - // tsc's narrowing): scalars read the snapshot's slots, refcounted - // payloads come out retained (+1). Box borrowed. - const c = emitter.emitExpr(e.value); - if (e.type.kind === "f64") return emitter.newTemp(e.type, `${c.name}->f64`); - if (e.type.kind === "bool") return emitter.newTemp(e.type, `${c.name}->b`); - if (e.type.kind === "string") { - return emitter.newTemp(e.type, `scr_str_retain((ScrStr *)${c.name}->payload)`); - } - if (e.type.kind === "object") { - return emitter.newTemp(e.type, `(${cType(e.type).trim()})${c.name}->retain_fn(${c.name}->payload)`); - } - throw new InternalCompilerError(`emitter bug: caughtNarrow to ${e.type.kind}`); - } - case "caughtToDyn": { - // The caught snapshot converting to a dyn value (an unknown - // slot) — the interned runtime-kind dispatch. Box borrowed; the - // result is a fresh tree (+1). Never throws. - const c = emitter.emitExpr(e.value); - return emitter.newTemp(e.type, `${emitter.caughtToDynHelper()}(${c.name})`); - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitIntrinsicExpr( - emitter: CEmitter, - e: ExprOf<"intrinsic">, -): Temp { - switch (e.kind) { - case "intrinsic": { - if (e.name === "module.await") { - // The module evaluator's dependency wait: park only while the - // promise is pending. A settled dependency continues in this - // turn (no JavaScript-await hop); a rejection rethrows. - const p = emitter.emitExpr(e.args[0]!); - emitter.line(`scr_module_await(${p.name});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - if (e.name === "promise.all") { - // The runtime countdown combinator: a pre-sized values array - // (filled per INPUT index by the per-kind store helper as - // entries fulfill) plus one subscription per entry — settled - // entries settle inline, the first rejection wins, later - // rejections count as handled. A void inner type passes no - // values array and the result fulfills void. - if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.all type"); - const entries = e.args[0]!; - if (entries.type.kind !== "array" || entries.type.elem.kind !== "promise") { - throw new InternalCompilerError("emitter bug: promise.all argument"); - } - const ps = emitter.emitExpr(entries); - if (e.type.inner.kind === "void") { - return emitter.newTemp(e.type, `scr_promise_all(${ps.name}, NULL, NULL)`); - } - if (e.type.inner.kind !== "array") throw new InternalCompilerError("emitter bug: promise.all result"); - const elem = e.type.inner.elem; - const store = - elem.kind === "f64" - ? "scr_promise_all_store_f64" - : elem.kind === "bool" - ? "scr_promise_all_store_bool" - : elem.kind === "string" - ? "scr_promise_all_store_str" - : "scr_promise_all_store_ref"; - // Entry array and values array both stay frame-owned; the - // combinator BORROWS them and retains what it keeps (the race - // convention). - const vals = emitter.newTemp(e.type.inner, emitter.arrNewC(elem, `(size_t)scr_arr_len(${ps.name})`)); - return emitter.newTemp(e.type, `scr_promise_all(${ps.name}, ${vals.name}, &${store})`); - } - if (e.name === "promise.reject") { - // A fresh promise rejected through the exception cell: the - // %Error-rooted reason moves in as the cell's OBJ payload - // (exactly the thrown-Error representation — catch instanceof - // and the uncaught printer see a thrown Error), and - // reject_pending moves the cell into the promise, wakes - // nothing (no waiters can exist yet), and enters the unhandled - // ledger until an await/then observes it. The cell is consumed - // immediately, so no pending check runs in between. - if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.reject type"); - const reason = emitter.emitExpr(e.args[0]!); - const t = e.args[0]!.type; - if (t.kind !== "object" && t.kind !== "dyn") { - throw new InternalCompilerError("emitter bug: promise.reject reason"); - } - const p = emitter.newTemp(e.type, `scr_promise_new()`); - emitter.moveTemp(reason); // the cell takes ownership - const rc = vAdapters(t); - if (t.kind === "dyn") { - // Preserve Error classification and checked-value identity - // through promise rejection and catch observers. - emitter.line( - `scr_dyn_throw(${reason.name});${emitter.srcComment(e.loc)}`, - ); - } else { - emitter.line( - `scr_throw_obj(${reason.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`, - ); - } - emitter.line(`scr_promise_reject_pending(${p.name});`); - return p; - } - if (e.name === "promise.resolve") { - // A fresh promise fulfilled immediately: void/f64/bool by - // value, strings and refs MOVE in (fulfill takes the +1 — the - // moveTemp keeps the frame from double-releasing), matching the - // async-return trampoline's fulfill exactly. No waiters exist - // yet, so the wake is a no-op. - if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.resolve type"); - const p = emitter.newTemp(e.type, `scr_promise_new()`); - if (e.args.length === 0) { - emitter.line(`scr_promise_fulfill_void(${p.name});${emitter.srcComment(e.loc)}`); - return p; - } - const v = emitter.emitExpr(e.args[0]!); - const t = e.args[0]!.type; - switch (t.kind) { - case "f64": - case "date": - emitter.line(`scr_promise_fulfill_f64(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`); - break; - case "bool": - emitter.line(`scr_promise_fulfill_bool(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`); - break; - case "string": - emitter.moveTemp(v); - emitter.line(`scr_promise_fulfill_str(${p.name}, ${v.name});${emitter.srcComment(e.loc)}`); - break; - default: { - const rc = vAdapters(t); - emitter.moveTemp(v); - emitter.line( - `scr_promise_fulfill_ref(${p.name}, ${v.name}, ${rc.retain}, ${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`, - ); - } - } - return p; - } - if (e.name === "promise.race") { - // A fresh result promise + one race_add per entry: settled - // entries settle it immediately (first add wins), pending ones - // park a callback waiter. The interned adapter converts each - // entry's payload to the result's inner type; entry temps stay - // frame-owned (race_add retains what it keeps). - if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.race type"); - const result = emitter.newTemp(e.type, `scr_promise_new()`); - for (const entry of e.args) { - if (entry.type.kind !== "promise") throw new InternalCompilerError("emitter bug: promise.race entry"); - const p = emitter.emitExpr(entry); - const adapter = emitter.raceAdapterFor(entry.type.inner, e.type.inner); - emitter.line(`scr_promise_race_add(${result.name}, ${p.name}, &${adapter});${emitter.srcComment(e.loc)}`); - } - return result; - } - // console.log and its stderr twin console.error share the ScrLogArg - // packing; only the runtime entry point (stream) differs. - const args = e.args.map((a) => emitter.emitExpr(a)); - const arr = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrLogArg ${arr}[${Math.max(args.length, 1)}];`); - args.forEach((a, i) => { - switch (a.type.kind) { - case "f64": - emitter.line(`${arr}[${i}].tag = SCR_ARG_F64; ${arr}[${i}].v.f = ${a.name};`); - break; - case "string": - emitter.line(`${arr}[${i}].tag = SCR_ARG_STR; ${arr}[${i}].v.s = ${a.name};`); - break; - case "bool": - emitter.line(`${arr}[${i}].tag = SCR_ARG_BOOL; ${arr}[${i}].v.b = ${a.name};`); - break; - default: - throw new InternalCompilerError(`${e.name} arg of type ${a.type.kind}`); - } - }); - const consoleFn = e.name === "console.error" ? "scr_console_error" : "scr_console_log"; - emitter.line(`${consoleFn}(${args.length}, ${arr});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - default: { - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitSerializationExpr( - emitter: CEmitter, - e: ExprOf<"jsonStringify" | "dynCheck">, -): Temp { - switch (e.kind) { - case "jsonStringify": { - // Type-directed serialization: the STATIC type picks an emitted - // serializer (interned per type) — no dyn, no runtime dispatch. The - // value temp is BORROWED (released with this statement's frame); - // the result string is owned (+1). Throws only over CYCLE-CAPABLE - // types (recursive records — the circular-structure TypeError) and - // dyn roots; everything else keeps the throw-free path. - const v = emitter.emitExpr(e.value); - // A dyn root: the runtime's dyn walker (scr_dyn_format_j — the %j - // serializer IS JSON.stringify over the checked-dynamic tree: number/string/bool/ - // null/array/object exact, dropped members omitted, a dropped ROOT - // becomes the text "undefined", a runtime handle inside the tree - // throws) — fallible, so the pending-exception check runs. - const compact = - e.value.type.kind === "dyn" - ? emitter.fallibleTemp(e.type, `scr_dyn_format_j(${v.name})`) - : (() => { - const helper = emitter.jsonWriteHelper(e.value.type); - const buf = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrJsonBuf ${buf}; scr_jb_init(&${buf});${emitter.srcComment(e.loc)}`); - emitter.line(`${helper}(&${buf}, ${v.name});`); - const t = emitter.newTemp(e.type, `scr_jb_finish(&${buf})`); - // 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. - if (emitter.traceAdapterC(e.value.type) !== null) emitter.emitPendingCheck(); - return t; - })(); - // A pretty-print form (`stringify(v, null, 2)`): the frontend - // resolved the space to a compile-time indent string (Node's - // clamp/truncate rules) riding as an extra property; the interned - // re-indenter rewrites the compact text with Node's gap algorithm. - // Compact temp stays frame-owned; the pretty string is a fresh +1. - const indent = (e as { indent?: string }).indent; - if (!indent) return compact; - const bytes = Buffer.from(indent, "utf8"); - return emitter.newTemp( - e.type, - `${emitter.jsonIndentHelper()}(${compact.name}, ${cStringLiteral(bytes)}, ${bytes.length})`, - ); - } - case "dynCheck": { - // The dynamic boundary: validate the checked-dynamic tree against the target type - // and BUILD the typed value (+1) — or throw a catchable - // TypeError-shaped, path-annotated string. The dyn temp is BORROWED; - // the result joins the frame BEFORE the pending check so an unwind - // releases the dummy (NULL for refcounted targets) harmlessly. - const dyn = emitter.emitExpr(e.value); - const helper = emitter.dynCheckHelper(e.type); - return emitter.fallibleTemp(e.type, `${helper}(${dyn.name}, NULL)`); - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitAsyncExpr( - emitter: CEmitter, - e: ExprOf<"yieldExpr" | "genResume" | "awaitExpr" | "awaitUnionExpr" | "newPromise" | "promiseWithResolvers">, -): Temp { - switch (e.kind) { - case "yieldExpr": { - // Park the operand in the generator's OUT slot (moved in, typed by - // the function's yield channel) and switch back to the resumer. - // Control returns at the next resume — possibly with an injected - // .throw payload or the GENRET sentinel pending, hence the check. - // The result is the .next(v) argument, moved out of the IN slot. - const gen = emitter.currentGenerator; - if (!gen) throw new InternalCompilerError("emitter bug: yieldExpr outside a generator body"); - if (e.value === null) throw new InternalCompilerError("emitter bug: yieldExpr with no operand (frontend fills undefined)"); - const v = emitter.emitExpr(e.value); - const yt = e.value.type; - if (e.awaited) emitter.line(`scr_async_gen_hop_done();`); - if (yt.kind === "f64" || yt.kind === "date") { - emitter.line(`scr_gen_yield_f64(${v.name});${emitter.srcComment(e.loc)}`); - } else if (yt.kind === "bool") { - emitter.line(`scr_gen_yield_bool(${v.name});${emitter.srcComment(e.loc)}`); - } else { - emitter.moveTemp(v); // the OUT slot takes ownership - emitter.line(`scr_gen_yield_ref(${v.name}, ${vAdapters(yt).release});${emitter.srcComment(e.loc)}`); - } - emitter.emitPendingCheck(); - switch (e.type.kind) { - case "void": - // An undefined next-channel: nothing to read (the frontend - // fences value-position yields on this channel). - return { name: "", type: e.type }; - case "f64": - case "date": - return emitter.newTemp(e.type, `scr_gen_take_in_f64()`); - case "bool": - return emitter.newTemp(e.type, `scr_gen_take_in_bool()`); - default: - // Refcounted channels (dyn included): the slot's +1 moves out. - return emitter.newTemp(e.type, `(${cType(e.type).trim()})scr_gen_take_in_ref()`); - } - } - case "genResume": { - // One consumer resume: park the sent value (typed per mode), hop - // into the fiber, propagate a body exception (pending check), and - // build the IteratorResult record through the interned helper. - const genT = e.gen.type; - if (genT.kind !== "generator") throw new InternalCompilerError("emitter bug: genResume on a non-generator"); - const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type; - if (resultT?.kind !== "record") throw new InternalCompilerError("emitter bug: genResume result is not an IteratorResult record"); - const g = emitter.emitExpr(e.gen); // borrowed for the calls below - const sendArg = (store: (a: Temp) => string): void => { - const a = emitter.emitExpr(e.arg!); - if (isRefCounted(e.arg!.type)) emitter.moveTemp(a); // the slot takes ownership - emitter.line(store(a)); - }; - if (genT.async) { - let call: string; - if (e.mode === "next") { - if (e.arg === null) { - if (genT.nextT.kind === "dyn") { - call = `scr_async_gen_next_ref(${g.name}, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_release_v)`; - } else { - call = `scr_async_gen_next_none(${g.name})`; - } - } else { - const a = emitter.emitExpr(e.arg); - const t = e.arg.type; - if (isRefCounted(t)) emitter.moveTemp(a); - call = t.kind === "f64" || t.kind === "date" - ? `scr_async_gen_next_f64(${g.name}, ${a.name})` - : t.kind === "bool" - ? `scr_async_gen_next_bool(${g.name}, ${a.name})` - : `scr_async_gen_next_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`; - } - } else if (e.mode === "return") { - if (e.arg === null) { - call = `scr_async_gen_return_none(${g.name})`; - } else { - const a = emitter.emitExpr(e.arg); - const t = e.arg.type; - if (isRefCounted(t)) emitter.moveTemp(a); - call = t.kind === "f64" || t.kind === "date" - ? `scr_async_gen_return_f64(${g.name}, ${a.name})` - : t.kind === "bool" - ? `scr_async_gen_return_bool(${g.name}, ${a.name})` - : `scr_async_gen_return_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`; - } - } else { - if (e.arg === null) throw new InternalCompilerError("emitter bug: async genResume throw with no payload"); - const a = emitter.emitExpr(e.arg); - const t = e.arg.type; - if (isRefCounted(t)) emitter.moveTemp(a); - if (t.kind === "date") { - throw new InternalCompilerError("emitter bug: Date async-generator throw reached backend"); - } else if (t.kind === "f64") { - emitter.line(`scr_throw_f64(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "bool") { - emitter.line(`scr_throw_bool(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "string") { - emitter.line(`scr_throw_str(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) { - const rc = vAdapters(t); - emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") { - const rc = vAdapters(t); - emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`); - } else if (t.kind === "dyn") { - emitter.line(`scr_dyn_throw(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "jsval") { - const rc = vAdapters(t); - const test = "scr_jsval_is_object"; - emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`); - } else { - const rc = vAdapters(t); - emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); - } - call = `scr_async_gen_throw(${g.name})`; - } - return emitter.newTemp(e.type, call); - } - if (e.mode === "next") { - if (e.arg === null) { - // Valueless resume: dyn channels read JS's undefined; unit - // channels have nothing to read. - if (genT.nextT.kind === "dyn") { - emitter.line(`scr_gen_in_ref(${g.name}, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_release_v);${emitter.srcComment(e.loc)}`); - } else { - emitter.line(`scr_gen_in_none(${g.name});${emitter.srcComment(e.loc)}`); - } - } else { - const nt = e.arg.type; - sendArg((a) => - nt.kind === "f64" || nt.kind === "date" ? `scr_gen_in_f64(${g.name}, ${a.name});${emitter.srcComment(e.loc)}` - : nt.kind === "bool" ? `scr_gen_in_bool(${g.name}, ${a.name});${emitter.srcComment(e.loc)}` - : `scr_gen_in_ref(${g.name}, ${a.name}, ${vAdapters(nt).release});${emitter.srcComment(e.loc)}`); - } - emitter.line(`scr_gen_resume(${g.name});`); - } else if (e.mode === "return") { - if (e.arg === null) { - emitter.line(`scr_gen_ret_none(${g.name});${emitter.srcComment(e.loc)}`); - } else { - const rt = e.arg.type; - sendArg((a) => - rt.kind === "f64" || rt.kind === "date" ? `scr_gen_ret_f64(${g.name}, ${a.name});${emitter.srcComment(e.loc)}` - : rt.kind === "bool" ? `scr_gen_ret_bool(${g.name}, ${a.name});${emitter.srcComment(e.loc)}` - : `scr_gen_ret_ref(${g.name}, ${a.name}, ${vAdapters(rt).release});${emitter.srcComment(e.loc)}`); - } - emitter.line(`scr_gen_resume_return(${g.name});`); - } else { - // .throw(e): park the payload in the CALLER's cell (the throw - // statement's exact kind dispatch), then resume — the runtime - // moves it into the fiber, or leaves it pending (non-suspended - // generators: the .throw call itself throws at the check below). - if (e.arg === null) throw new InternalCompilerError("emitter bug: genResume throw with no payload"); - const a = emitter.emitExpr(e.arg); - const t = e.arg.type; - if (isRefCounted(t)) emitter.moveTemp(a); // the cell takes ownership - if (t.kind === "date") { - throw new InternalCompilerError("emitter bug: Date generator throw reached backend"); - } else if (t.kind === "f64") { - emitter.line(`scr_throw_f64(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "bool") { - emitter.line(`scr_throw_bool(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "string") { - emitter.line(`scr_throw_str(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) { - const rc = vAdapters(t); - emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") { - const rc = vAdapters(t); - emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`); - } else if (t.kind === "dyn") { - emitter.line(`scr_dyn_throw(${a.name});${emitter.srcComment(e.loc)}`); - } else if (t.kind === "jsval") { - const rc = vAdapters(t); - const test = "scr_jsval_is_object"; - emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`); - } else { - const rc = vAdapters(t); - emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); - } - emitter.line(`scr_gen_resume_throw(${g.name});`); - } - const helper = genResultThunkFor(emitter, genT, resultT); - // The record builds before the check so an unwind (a propagated - // body exception) releases it as the frame's never-read dummy. - return emitter.fallibleTemp(e.type, `${helper}(${g.name})`); - } - case "awaitExpr": { - // Parks the fiber until the promise settles; rejected promises - // re-throw here (hence the pending check). Promise temp borrowed; - // refcounted results arrive +1 and join the frame pre-check so an - // unwind releases the dummy (NULL) harmlessly. - const pr = emitter.emitExpr(e.value); - let read: string; - switch (e.type.kind) { - case "f64": - case "date": - read = `scr_await_f64(${pr.name})`; - break; - case "bool": - read = `scr_await_bool(${pr.name})`; - break; - case "string": - read = `scr_await_str(${pr.name})`; - break; - case "void": { - emitter.line(`scr_await_void(${pr.name});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - case "dyn": - // The checked-dynamic tree-crossing await: void fulfillments (a boxed - // promise that skipped its adapter) answer the - // undefined VALUE, never NULL. - read = `scr_await_dyn(${pr.name})`; - break; - default: - read = `(${cType(e.type).trim()})scr_await_ref(${pr.name})`; - } - return emitter.fallibleTemp(e.type, read); - } - case "awaitUnionExpr": { - // Await of a promise-or-absent union: the promise arm awaits like - // awaitExpr (parks, re-throws rejections); a unit arm takes exactly - // one microtask hop (JS: await of a non-thenable) and yields - // itself. The union temp is borrowed; a value-carrying result joins - // the frame BEFORE the pending check so an unwind releases the - // dummy (a union box holding the never-read NULL payload, or NULL) - // harmlessly. - if (e.value.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: awaitUnion of a non-union"); - } - const def = emitter.unionsById.get(e.value.type.unionId); - const promiseArm = def?.arms[e.promiseTag]; - if (!def || promiseArm?.kind !== "promise") { - throw new InternalCompilerError("emitter bug: awaitUnion arm is not a promise"); - } - const inner = promiseArm.inner; - const u = emitter.emitExpr(e.value); - const peek = `(ScrPromise *)scr_union_peek(${u.name})`; - if (e.type.kind === "void") { - emitter.line( - `if (${u.name}->tag == ${e.promiseTag}) scr_await_void(${peek}); else scr_await_hop();${emitter.srcComment(e.loc)}`, - ); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: awaitUnion result is neither void nor a union"); - } - const resDef = emitter.unionsById.get(e.type.unionId); - if (!resDef) throw new InternalCompilerError("emitter bug: awaitUnion result union unknown"); - const resTagOf = (arm: (typeof resDef.arms)[number]): number => { - const tag = resDef.arms.findIndex((a) => typeEquals(a, arm)); - if (tag < 0) throw new InternalCompilerError("emitter bug: awaitUnion result arm missing"); - return tag; - }; - const innerTag = resTagOf(inner); - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)} = NULL;${emitter.srcComment(e.loc)}`); - emitter.currentFrame().push({ name, type: e.type }); - emitter.line(`if (${u.name}->tag == ${e.promiseTag}) {`); - emitter.indent++; - let wrap: string; - switch (inner.kind) { - case "f64": - wrap = `scr_union_new_f64(${innerTag}, scr_await_f64(${peek}))`; - break; - case "bool": - wrap = `scr_union_new_bool(${innerTag}, scr_await_bool(${peek}))`; - break; - case "string": - wrap = `scr_union_new_ref(${innerTag}, scr_await_str(${peek}), scr_str_retain_v, scr_str_release_v, NULL)`; - break; - default: { - const v = vAdapters(inner); - wrap = `scr_union_new_ref(${innerTag}, scr_await_ref(${peek}), ${v.retain}, ${v.release}, ${emitter.traceArgC(inner)})`; - } - } - emitter.line(`${name} = ${wrap};`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`scr_await_hop();`); - const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : [])); - if (unitTags.length === 1) { - emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[unitTags[0]!]!))};`); - } else { - emitter.line(`switch (${u.name}->tag) {`); - for (const t of unitTags) { - emitter.line(`case ${t}: ${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[t]!))}; break;`); - } - emitter.line(`default: break;`); - emitter.line(`}`); - } - emitter.indent--; - emitter.line(`}`); - emitter.emitPendingCheck(); - return { name, type: e.type }; - } - case "newPromise": { - // Pending promise + resolve closure, executor run synchronously - // (its throw rejects — handled inside the runtime helper, so no - // pending check here). Executor/resolve temps are frame-owned. - if (e.type.kind !== "promise") throw new InternalCompilerError("emitter bug: newPromise type"); - const inner = e.type.inner; - const p = emitter.newTemp(e.type, `scr_promise_new()`); - // Zero-param executor: no resolve exists — a forever-pending - // promise unless the executor throws (which rejects it). - if (e.executor.type.kind === "func" && e.executor.type.params.length === 0) { - const exec0 = emitter.emitExpr(e.executor); - emitter.line( - `scr_promise_run_executor0(${p.name}, ${exec0.name});${emitter.srcComment(e.loc)}`, - ); - return p; - } - let mk: string; - switch (inner.kind) { - case "f64": - case "date": - mk = `scr_make_resolve(${p.name}, 0)`; - break; - case "bool": - mk = `scr_make_resolve(${p.name}, 1)`; - break; - case "string": - mk = `scr_make_resolve(${p.name}, 2)`; - break; - case "void": - mk = `scr_make_resolve(${p.name}, 3)`; - break; - default: - mk = `scr_make_resolve_fn(${p.name}, (void *)&${emitter.resolveThunkFor(inner)})`; - } - const resolve = emitter.newTemp( - { kind: "func", params: inner.kind === "void" ? [] : [inner], ret: { kind: "void" } }, - mk, - ); - // Two-param executor: reject is a runtime-provided closure rejecting - // the promise with its Error reason (OBJ payload — catch instanceof - // and the uncaught printer see exactly a thrown Error). First settle - // wins in the runtime; both closures' +1 move into the call. - if (e.executor.type.kind === "func" && e.executor.type.params.length === 2) { - const reject = emitter.newTemp( - { kind: "func", params: [{ kind: "object", className: "%Error" }], ret: { kind: "void" } }, - `scr_make_reject(${p.name})`, - ); - const exec2 = emitter.emitExpr(e.executor); - emitter.moveTemp(resolve); - emitter.moveTemp(reject); - emitter.line( - `scr_promise_run_executor2(${p.name}, ${exec2.name}, ${resolve.name}, ${reject.name});${emitter.srcComment(e.loc)}`, - ); - return p; - } - const exec = emitter.emitExpr(e.executor); - // The executor is a compiled closure and OWNS its params (it - // releases them on exit) — resolve's +1 moves into the call. The - // executor closure itself is borrowed (frame-released here). - emitter.moveTemp(resolve); - emitter.line( - `scr_promise_run_executor(${p.name}, ${exec.name}, ${resolve.name});${emitter.srcComment(e.loc)}`, - ); - return p; - } - case "promiseWithResolvers": { - // The newPromise pieces without an executor: a pending promise, - // its runtime resolve closure (typed per the inner kind), and - // the reject closure, written into the fresh record. Closure +1s - // move into the record's fields; never throws. - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: promiseWithResolvers type"); - const shape = emitter.recordsById.get(e.type.shapeId); - const promT = shape?.fields.find((f) => f.name === "promise")?.type; - const resolveT = shape?.fields.find((f) => f.name === "resolve")?.type; - const rejectT = shape?.fields.find((f) => f.name === "reject")?.type; - if (!shape || promT?.kind !== "promise" || !resolveT || !rejectT) { - throw new InternalCompilerError("emitter bug: promiseWithResolvers record shape"); - } - const inner = promT.inner; - const p = emitter.newTemp(promT, `scr_promise_new()`); - let mk: string; - switch (inner.kind) { - case "f64": - case "date": - mk = `scr_make_resolve(${p.name}, 0)`; - break; - case "bool": - mk = `scr_make_resolve(${p.name}, 1)`; - break; - case "string": - mk = `scr_make_resolve(${p.name}, 2)`; - break; - case "void": - mk = `scr_make_resolve(${p.name}, 3)`; - break; - default: - mk = `scr_make_resolve_fn(${p.name}, (void *)&${emitter.resolveThunkFor(inner)})`; - } - const resolve = emitter.newTemp(resolveT, mk); - const reject = emitter.newTemp(rejectT, `scr_make_reject(${p.name})`); - const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - // The promise's +1 moves into the record; the record's own read - // of it at the call site retains per field access as usual. - emitter.moveTemp(p); - emitter.moveTemp(resolve); - emitter.moveTemp(reject); - emitter.line(`${rec.name}->${mangleField("promise")} = ${p.name};`); - emitter.line(`${rec.name}->${mangleField("resolve")} = ${resolve.name};`); - emitter.line(`${rec.name}->${mangleField("reject")} = ${reject.name};`); - return rec; - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -function emitJsInteropExpr( - emitter: CEmitter, - e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">, -): Temp { - switch (e.kind) { - case "jsMarshal": { - // Static → island (--dynamic only). Primitives by value; JSON-safe - // composites deep-copy through the emitted type-directed serializer - // and the engine's JSON parser (documented aliasing divergence). - // Operand borrowed; result +1. from_json cannot fail on this - // machine-produced JSON but reports engine surprises via NULL + - // pending — check like a may-throw so the dummy unwinds cleanly. - const v = emitter.emitExpr(e.value); - switch (e.value.type.kind) { - case "f64": - return emitter.newTemp(e.type, `scr_jsval_from_f64(${v.name})`); - case "bool": - return emitter.newTemp(e.type, `scr_jsval_from_bool(${v.name})`); - case "string": - return emitter.newTemp(e.type, `scr_jsval_from_str(${v.name})`); - case "dyn": - // A CHECKED-DYNAMIC (dyn) value entering the island: deep - // copy, data kinds only — boxed functions/handles/promises - // throw the catchable TypeError in the runtime. - return emitter.fallibleTemp(e.type, `scr_jsval_from_dyn(${v.name})`); - case "bytes": - // A typed array crossing IN: an engine typed array of the same - // element kind — a COPY (the boundary's copy stance). - return emitter.fallibleTemp(e.type, `scr_jsval_from_bytes(${v.name})`); - case "url": - // A URL crossing IN: an engine URL instance built from href. - return emitter.fallibleTemp(e.type, `scr_jsval_from_url(${v.name})`); - case "promise": { - // 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. - const tag = islandPromisePayloadTag(e.value.type.inner); - if (!tag) throw new InternalCompilerError("emitter bug: jsMarshal of a promise outside the bridge payload domain"); - const tagC = { - void: "SCR_ISLP_VOID", f64: "SCR_ISLP_F64", bool: "SCR_ISLP_BOOL", - string: "SCR_ISLP_STR", jsval: "SCR_ISLP_JSVAL", jsvalArr: "SCR_ISLP_JSVAL_ARR", - }[tag]; - return emitter.fallibleTemp(e.type, `scr_jsval_from_promise(scr_promise_retain(${v.name}), ${tagC})`); - } - case "func": { - // A closure entering the island as a host function (the - // package-callback pattern). from_closure retains the closure; - // the engine's finalizer releases it at teardown — which runs - // before the RC audit. The per-signature adapter gives the - // runtime one uniform call shape over the closure ABI: the - // interned (arity, return) adapters for the all-'any' shape, - // or a typed adapter converting each incoming argument to the - // param's static type through the exit machinery. - const fn = e.value.type; - const adapter = canMarshalFuncIntoIsland(fn) - ? emitter.islandAdapter( - fn.params.length, - fn.ret.kind as "void" | "jsval" | "f64" | "bool" | "string", - ) - : emitter.islandTypedAdapter(fn); - // ISLAND-REST closures encode a NEGATIVE arity: the wrapper - // pads the leading declared params and 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; - return emitter.newTemp( - e.type, - `scr_jsval_from_closure(${v.name}, ${arity}, ${adapter})`, - ); - } - default: { - const helper = emitter.jsonWriteHelper(e.value.type); - const buf = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrJsonBuf ${buf}; scr_jb_init(&${buf});${emitter.srcComment(e.loc)}`); - emitter.line(`${helper}(&${buf}, ${v.name});`); - const json = emitter.newTemp(STRING, `scr_jb_finish(&${buf})`); - return emitter.fallibleTemp(e.type, `scr_jsval_from_json(${json.name})`); - } - } - } - case "jsOp": { - // Island operation: JS semantics via the engine (prelude helper - // closures), never C reimplementations. jsval args are borrowed - // frame temps; jsval/string results +1. Engine exceptions bridge - // into the cell — pending checks after every fallible op. - const args = e.args.map((a) => emitter.emitExpr(a)); - const a = (i: number) => args[i]!.name; - const nameSym = () => `(ScrStr *)&${emitter.internLiteral(e.name!)}`; - const finishFallible = (call: string): Temp => emitter.fallibleTemp(e.type, call); - const argPack = (list: string[]): string => { - if (list.length === 0) return "NULL"; - const arr = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrJsval *${arr}[] = { ${list.join(", ")} };`); - return arr; - }; - switch (e.op) { - case "add": case "sub": case "mul": case "div": case "mod": case "pow": { - const c = `SCR_JSOP_${e.op.toUpperCase()}`; - return finishFallible(`scr_jsval_binop(${c}, ${a(0)}, ${a(1)})`); - } - case "lt": case "le": case "gt": case "ge": case "eq": case "neq": { - const c = `SCR_JSOP_${e.op.toUpperCase()}`; - return finishFallible(`(scr_jsval_cmp(${c}, ${a(0)}, ${a(1)}) == 1)`); - } - case "instanceOf": - return finishFallible(`(scr_jsval_instance_of(${a(0)}, ${a(1)}) == 1)`); - case "neg": - return finishFallible(`scr_jsval_neg(${a(0)})`); - case "plus": - return finishFallible(`scr_jsval_plus(${a(0)})`); - case "truthy": - return emitter.newTemp(e.type, `(scr_jsval_truthy(${a(0)}) != 0)`); - case "not": - return emitter.newTemp(e.type, `(scr_jsval_truthy(${a(0)}) == 0)`); - case "typeof": - return emitter.newTemp(e.type, `scr_jsval_typeof(${a(0)})`); - case "toStr": - return finishFallible(`scr_jsval_to_str(${a(0)})`); - case "getProp": - return finishFallible(`scr_jsval_get_prop(${a(0)}, ${nameSym()})`); - case "globalGet": - return finishFallible(`scr_jsval_global_get(${nameSym()})`); - case "setProp": - emitter.line(`scr_jsval_set_prop(${a(0)}, ${nameSym()}, ${a(1)});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - case "getIdx": - return finishFallible(`scr_jsval_get_idx(${a(0)}, ${a(1)})`); - case "iterNew": - return finishFallible(`scr_jsval_iter_new(${a(0)})`); - case "setIdx": - emitter.line(`scr_jsval_set_idx(${a(0)}, ${a(1)}, ${a(2)});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - case "optCallMethod": { - const pack = argPack(args.slice(1).map((x) => x.name)); - return finishFallible( - `scr_jsval_opt_call_method(${a(0)}, ${nameSym()}, ${args.length - 1}, ${pack})`, - ); - } - case "callMethod": { - const pack = argPack(args.slice(1).map((x) => x.name)); - return finishFallible( - `scr_jsval_call_method(${a(0)}, ${nameSym()}, ${args.length - 1}, ${pack})`, - ); - } - case "callFnThis": { - const pack = argPack(args.slice(2).map((x) => x.name)); - return finishFallible( - `scr_jsval_call_this(${a(0)}, ${a(1)}, ${args.length - 2}, ${pack})`, - ); - } - case "callFn": { - const pack = argPack(args.slice(1).map((x) => x.name)); - return finishFallible(`scr_jsval_call(${a(0)}, ${args.length - 1}, ${pack})`); - } - case "callSpread": - // Spread application (`f(...pre, ...s)`): the prelude helper's - // real spread syntax — iterator protocols are the engine's - // own, the guards front-run V8's spread-call TypeError texts - // (the name literal is the spread expression's spelling). - return finishFallible(`scr_jsval_call_spread(${a(0)}, ${a(1)}, ${a(2)}, ${nameSym()})`); - case "construct": { - // `new X(...)` on an island callee: JS_CallConstructor. - const pack = argPack(args.slice(1).map((x) => x.name)); - return finishFallible(`scr_jsval_construct(${a(0)}, ${args.length - 1}, ${pack})`); - } - case "objLit": { - const pack = argPack(args.map((x) => x.name)); - return emitter.newTemp(e.type, `scr_jsval_obj_lit(${args.length / 2}, ${pack})`); - } - case "tplStrings": { - const pack = argPack(args.map((x) => x.name)); - return emitter.newTemp(e.type, `scr_jsval_tpl_strings(${args.length / 2}, ${pack})`); - } - case "objSpread": - // Spread completion: engine CopyDataProperties (getters can - // throw — fallible); answers the target (+1). - return finishFallible(`scr_jsval_obj_spread(${a(0)}, ${a(1)})`); - case "defineGetter": - // Getter completion for an island literal: defines key (a(1)) - // on obj (a(0)) as an engine getter invoking a(2); answers the - // object (+1) for chaining. - return emitter.newTemp(e.type, `scr_jsval_define_getter(${a(0)}, ${a(1)}, ${a(2)})`); - case "arrLit": { - const pack = argPack(args.map((x) => x.name)); - return emitter.newTemp(e.type, `scr_jsval_arr_lit(${args.length}, ${pack})`); - } - case "undefLit": - return emitter.newTemp(e.type, `scr_jsval_undefined()`); - case "nullLit": - return emitter.newTemp(e.type, `scr_jsval_null()`); - default: { - const _exhaustive: never = e.op; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } - } - case "jsExit": { - // Island → static validated exit. Primitives extract strictly (no - // coercion — a non-number refuses to exit as number); composites - // round-trip engine JSON.stringify → json.parse → the existing - // dynCheck walker, inheriting its width tolerance and path-annotated - // failures. Every step is a may-throw with the standard pending - // discipline; intermediate temps are frame-owned. - const v = emitter.emitExpr(e.value); - switch (e.type.kind) { - case "f64": - case "bool": { - const name = `sc_t${emitter.tempCounter++}`; - const ctype = e.type.kind === "f64" ? "double" : "bool"; - const fn = e.type.kind === "f64" ? "scr_jsval_exit_f64" : "scr_jsval_exit_bool"; - emitter.line(`${ctype} ${name} = 0;${emitter.srcComment(e.loc)}`); - emitter.line(`${fn}(${v.name}, &${name});`); - emitter.emitPendingCheck(); - return { name, type: e.type }; - } - case "string": - return emitter.fallibleTemp(e.type, `scr_jsval_exit_str(${v.name})`); - case "bytes": - // Uint8Array exit: kind-checked, copied out (+1) — engine - // Buffers pass (they ARE Uint8Arrays). The frontend only - // emits u8 targets (canExitIslandToType). - return emitter.fallibleTemp(e.type, `scr_jsval_exit_bytes(${v.name})`); - default: { - // `any[]`-declared slot: the engine array exits Array.isArray- - // gated, elements BY REFERENCE (identity crosses; the spine is - // a snapshot copy). JSON-safe element types keep the round - // trip below. - if (e.type.kind === "array" && e.type.elem.kind === "jsval") { - return emitter.fallibleTemp(e.type, `scr_jsval_exit_jsval_arr(${v.name})`); - } - // An undefined-armed union target: the engine's undefined takes - // the undefined arm FIRST — JSON cannot spell it (to_json would - // refuse the exit) — then null and data ride the round trip - // into the union's dynCheck like any composite (or, for the - // `any[] | undefined` defaulted-parameter spelling, the - // jsval-element array exit wrapped into the data arm). - const undefTag = e.type.kind === "union" ? undefinedArmTag(e.type, emitter.unionsById) : -1; - if (e.type.kind === "union" && undefTag >= 0) { - const name = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(e.type, name)};`); - emitter.line(`if (scr_jsval_is_undefined(${v.name})) {`); - emitter.indent++; - emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, undefTag)};`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.frames.push([]); - const unionDef = emitter.unionsById.get(e.type.unionId); - const dataArms = unionDef ? unionDef.arms.flatMap((a, i) => (isUnitType(a) ? [] : [{ a, i }])) : []; - const jsvalArr = dataArms.length === 1 && dataArms[0]!.a.kind === "array" && dataArms[0]!.a.elem.kind === "jsval" ? dataArms[0]! : null; - if (jsvalArr) { - // The `any[] | undefined` defaulted-parameter spelling: - // the engine array exits BY REFERENCE into the data arm. - const arr = emitter.fallibleTemp(jsvalArr.a, `scr_jsval_exit_jsval_arr(${v.name})`); - emitter.moveTemp(arr); - emitter.line(`${name} = scr_union_new_ref(${jsvalArr.i}, ${arr.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL);`); - } else { - const json = emitter.fallibleTemp(STRING, `scr_jsval_to_json(${v.name})`); - const dom = emitter.fallibleTemp(DYN, `scr_json_parse(${json.name})`); - const out = emitter.fallibleTemp(e.type, `${emitter.dynCheckHelper(e.type)}(${dom.name}, NULL)`); - emitter.moveTemp(out); - emitter.line(`${name} = ${out.name};`); - } - emitter.releaseFrame(emitter.frames.pop()!); - emitter.indent--; - emitter.line(`}`); - emitter.currentFrame().push({ name, type: e.type }); - return { name, type: e.type }; - } - const json = emitter.fallibleTemp(STRING, `scr_jsval_to_json(${v.name})`); - const dom = emitter.fallibleTemp(DYN, `scr_json_parse(${json.name})`); - return emitter.fallibleTemp(e.type, `${emitter.dynCheckHelper(e.type)}(${dom.name}, NULL)`); - } - } - } - case "jsBridgePromise": { - // Island → static promise bridge: a fresh pending ScrPromise the - // engine promise settles (fulfillment = retained jsval cell or - // void; rejection = the bridged reason). Operand borrowed; the +1 - // promise joins the frame. Fails only on an engine-level surprise - // minting the subscription — pending check like other island ops. - const v = emitter.emitExpr(e.value); - const payload = - e.type.kind === "promise" && e.type.inner.kind === "void" - ? "SCR_ISLP_VOID" - : e.type.kind === "promise" && e.type.inner.kind === "array" && e.type.inner.elem.kind === "jsval" - ? "SCR_ISLP_JSVAL_ARR" // `any[]` fulfillment: the Array.isArray-gated by-reference exit at settle - : "SCR_ISLP_JSVAL"; - return emitter.fallibleTemp(e.type, `scr_jsval_bridge_promise(${v.name}, ${payload})`); - } - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -type LibCallExpr = ExprOf<"libCall">; -type LibCallPrefixOf = T extends `${infer Prefix}.${string}` - ? Prefix - : never; -type LibCallPrefix = LibCallPrefixOf; - -interface LibCallState { - emitter: CEmitter; - e: LibCallExpr; - args: Temp[]; - arg: (index: number) => string; - finish: (call: string) => Temp; -} - -function emitWebLibCall(state: LibCallState): Temp { - const { e, emitter, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "fetch.start": - emitter.usesTimers = true; - return finish(`scr_fetch_static(${arg(0)}, ${arg(1)})`); - case "fetch.responseNew": - return finish(`scr_fetch_response_new(${arg(0)}, ${arg(1)})`); - case "fetch.responseArrayBuffer": - return finish(`scr_fetch_response_array_buffer(${arg(0)})`); - case "fetch.responseJson": - return finish(`scr_fetch_response_json(${arg(0)})`); - case "fetch.responseText": - case "fetch.responseBytes": { - if (e.type.kind !== "promise") { - throw new InternalCompilerError(`emitter bug: ${fn} result`); - } - const runtimeFn = - fn === "fetch.responseText" - ? "scr_fetch_response_text" - : "scr_fetch_response_bytes"; - const source = emitter.newTemp( - { kind: "promise", inner: DYN }, - `${runtimeFn}(${arg(0)})`, - ); - const result = emitter.newTemp(e.type, `scr_promise_new()`); - const adapter = dynPromiseAdapter(emitter, e.type.inner); - emitter.line( - `scr_promise_race_add(${result.name}, ${source.name}, &${adapter});${emitter.srcComment(e.loc)}`, - ); - return result; - } - case "fetch.abortControllerNew": - return finish(`scr_fetch_abort_controller_new()`); - case "fetch.abortTimeout": - emitter.usesTimers = true; - return finish(`scr_fetch_abort_timeout(${arg(0)})`); - case "fetch.abortNow": - return finish(`scr_fetch_abort_now(${arg(0)})`); - case "fetch.abortAny": - return finish(`scr_fetch_abort_any(${arg(0)})`); - case "fetch.streamNew": - emitter.usesTimers = true; - return finish(`scr_fetch_stream_new(${arg(0)})`); - case "fetch.streamFrom": - emitter.usesTimers = true; - if (e.args[0]!.type.kind === "array") { - const adapter = streamFromArrayAdapter(emitter, e.args[0]!.type); - return finish( - `scr_fetch_stream_from_array(${arg(0)}, &${adapter})`, - ); - } - if (e.args[0]!.type.kind === "bytes") { - return finish(`scr_fetch_stream_from_bytes(${arg(0)})`); - } - if (e.args[0]!.type.kind === "string") { - return finish(`scr_fetch_stream_from_string(${arg(0)})`); - } - return finish(`scr_fetch_stream_from(${arg(0)})`); - case "fetch.readerRead": { - if (e.type.kind !== "promise" || e.type.inner.kind !== "record") { - throw new InternalCompilerError("emitter bug: fetch.readerRead result"); - } - const source = emitter.newTemp( - { kind: "promise", inner: DYN }, - `scr_fetch_reader_read(${arg(0)})`, - ); - const result = emitter.newTemp(e.type, `scr_promise_new()`); - const adapter = dynPromiseAdapter(emitter, e.type.inner); - emitter.line( - `scr_promise_race_add(${result.name}, ${source.name}, &${adapter});${emitter.srcComment(e.loc)}`, - ); - return result; - } - case "island.eval": - // --dynamic builds only (the frontend fences the intrinsic, so - // scr_island_eval is always linked when this emits). Borrows - // the source; returns +1 String(result), or bridges the island - // exception into the cell (may-throw seed set). - return finish(`scr_island_eval(${arg(0)})`); - case "island.import": - // --dynamic builds only. Loads an EMBEDDED npm module (main - // registered the table before %main ran) through the island's - // module loader — cached by the engine, so one evaluation per - // module — and takes one export as an owned jsval. A package's - // top-level throw bridges catchably (may-throw seed set), and a - // MISSING named export throws Node's link-time SyntaxError - // naming the written specifier (arg 2). - return finish(`scr_jsval_import(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "island.importDyn": - // --dynamic builds only. Dynamic import(): the island's module - // system answers an ENGINE promise of the namespace — load and - // evaluation failures REJECT it (Node's shape), so the call - // itself never throws; the frontend's jsBridgePromise carries - // the settlement across. +1 jsval holding the engine promise. - return finish(`scr_jsval_import_dyn(${arg(0)})`); - case "island.castFail": - // The deferred boundary failure: the island value was - // evaluated (arg 0 — its side effects are real), the throw is - // unconditional (catchable TypeError naming the target type), - // and the typed dummy is a NULL promise the pending check - // abandons (releases are NULL-tolerant). - return finish(`(scr_jsval_cast_fail(${arg(0)}, ${arg(1)}), NULL)`); - case "json.parseReviver": - return finish(`scr_json_parse_reviver(${arg(0)}, ${arg(1)})`); - case "json.stringifyReplacer": - return finish(`scr_json_stringify_replacer(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "json.parse": - // Borrows the text; returns +1 on a fresh dyn, or throws a - // catchable SyntaxError-shaped string (may-throw seed set). - return finish(`scr_json_parse(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: web libCall dispatch for ${fn}`); - } -} - -function emitDynamicLibCall(state: LibCallState): Temp { - const { e, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "dyn.defineProps": - // Object.defineProperties over dyn values: both borrowed, - // result the target (+1); throws catchably (may-throw seed). - return finish(`scr_dyn_define_props(${arg(0)}, ${arg(1)})`); - case "dyn.defineProperty": - return finish(`scr_dyn_define_property(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.getOwnPropertyDescriptor": - return finish(`scr_dyn_get_own_property_descriptor(${arg(0)}, ${arg(1)})`); - case "dyn.arrayProtoCall": - return finish(`scr_dyn_array_proto_call(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.hasKey": - // `k in v` with a runtime key: the dyn presence answer (both - // borrowed, no allocation, never throws). - return finish(`scr_dyn_has_key(${arg(0)}, ${arg(1)})`); - case "dyn.keySet": - // Keyed write on a dyn receiver: all three borrowed (the - // member retains the value in); throws Node's TypeErrors on - // non-object receivers (may-throw seed set). - return finish(`scr_dyn_key_set(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.keySetComputed": - return finish(`scr_dyn_key_set_computed(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.keyDelete": - return finish(`scr_dyn_key_delete(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.globalSymbolGet": - return finish(`scr_dyn_global_symbol_get(${arg(0)})`); - case "dyn.globalSymbolSet": - return finish(`scr_dyn_global_symbol_set(${arg(0)}, ${arg(1)})`); - case "dyn.globalSymbolHas": - return finish(`scr_dyn_global_symbol_has(${arg(0)})`); - case "dyn.globalSymbolDelete": - return finish(`scr_dyn_global_symbol_delete(${arg(0)})`); - case "dyn.typedRefIs": - return finish(`scr_dyn_typed_ref_is_key(${arg(0)}, ${arg(1)})`); - case "dyn.iterPack": - // Destructuring/for-of pack over a dyn source: both borrowed, - // fresh array +1; throws V8's not-iterable TypeError on - // non-iterable dyn kinds and drains wrapped engine values - // through the engine's protocol (may-throw seed set). - return finish(`scr_dyn_iter_pack(${arg(0)}, ${arg(1)})`); - case "dyn.mapSeedEntries": - return finish(`scr_dyn_map_seed_entries(${arg(0)})`); - case "dyn.mapSeedEntry": - return finish(`scr_dyn_map_seed_entry(${arg(0)})`); - case "dyn.arrLen": - // The for-of pack's length (borrowed; never throws). - return finish(`scr_dyn_arr_len(${arg(0)})`); - case "dyn.arrAt": - // The for-of pack's element at index (+1; never throws — the - // undefined singleton past the end). - return finish(`scr_dyn_arr_at(${arg(0)}, ${arg(1)})`); - case "dyn.typeof": - // Bare typeof on a dyn value: the dyn kind's JS answer (+1). - return finish(`scr_dyn_typeof(${arg(0)})`); - case "dyn.objectTag": - return finish(`scr_dyn_object_tag(${arg(0)})`); - case "dyn.freeze": return finish(`scr_dyn_freeze(${arg(0)})`); - case "dyn.isFrozen": return finish(`scr_dyn_is_frozen(${arg(0)})`); - case "dyn.nativeSetNew": return finish(`scr_dyn_native_set_new(${arg(0)})`); - case "dyn.nativeSetIs": return finish(`scr_dyn_native_set_is(${arg(0)})`); - case "dyn.nativeUrlIs": return finish(`scr_dyn_native_url_is(${arg(0)})`); - case "dyn.nativeRegexIs": return finish(`scr_dyn_native_regex_is(${arg(0)})`); - case "dyn.toString": - // Receiver-kind-dispatched toString (+1); throws Node's - // TypeError on undefined/null and the "is not a function" - // TypeError on null-prototype dictionaries (may-throw seed - // set) — args[2] carries the call's source spelling. - return finish(`scr_dyn_to_string_argument(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.toStringCoerce": - // +1 string or NULL with the exception pending (user - // toString/valueOf throws propagate). Borrows the dyn. - return finish(`scr_dyn_string_coerce_js(${arg(0)})`); - case "dyn.numberConstructor": - return finish(`scr_dyn_number_constructor(${arg(0)})`); - case "dyn.toNumberCoerce": - // JS ToNumber (number-hint valueOf/toString protocol). A - // thrown hook leaves the exception pending; the may-throw - // epilogue abandons the NaN dummy. - return finish(`scr_dyn_number_coerce(${arg(0)})`); - case "dyn.add": - return finish(`scr_dyn_add(${arg(0)}, ${arg(1)})`); - case "dyn.proxyNew": - return finish(`scr_dyn_proxy_new(${arg(0)}, ${arg(1)})`); - case "global.native": - return finish(`scr_global_native(${arg(0)})`); - case "global.undefRead": - // A declare-d const nothing defines: Node's catchable - // ReferenceError at the access (always throws — the typed - // dummy is abandoned by the pending check's unwind; releases - // are NULL-tolerant). Borrows the name string. - return finish( - `(scr_undef_global_read(${arg(0)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "dyn.this": - return finish(`scr_dyn_this_get()`); - case "dyn.fromEntries": - return finish(`scr_dyn_from_entries(${arg(0)})`); - case "weakSet.new": - return finish(`scr_weak_set_new(${arg(0)})`); - case "weakMap.is": - return finish(`scr_weak_map_is(${arg(0)})`); - case "weakSet.is": - return finish(`scr_weak_set_is(${arg(0)})`); - case "weakMap.new": - return finish(`scr_weak_map_new(${arg(0)})`); - case "arrayBuffer.new": - return finish(`scr_array_buffer_new(${arg(0)})`); - case "ffi.argument": return finish(`scr_ffi_argument(${arg(0)}, ${arg(1)})`); - case "ffi.memoryModule": - return finish(`scr_ffi_memory_module(${arg(0)})`); - case "arrayBuffer.is": - return finish(`scr_array_buffer_is(${arg(0)})`); - case "arrayBuffer.isView": - return finish(`scr_array_buffer_is_view(${arg(0)})`); - case "arrayBuffer.byteLengthGetter": - return finish(`scr_array_buffer_byte_length_getter()`); - case "arrayBuffer.byteLengthDescriptor": - return finish(`scr_array_buffer_byte_length_descriptor(${arg(0)})`); - case "arrayBuffer.viewU8C": - return finish(`scr_array_buffer_view_u8c(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewI8": - return finish(`scr_array_buffer_view_i8(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewU16": - return finish(`scr_array_buffer_view_u16(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewI16": - return finish(`scr_array_buffer_view_i16(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewU8": - return finish(`scr_array_buffer_view_u8(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewU32": - return finish(`scr_array_buffer_view_u32(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewI32": - return finish(`scr_array_buffer_view_i32(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewF32": - return finish(`scr_array_buffer_view_f32(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewF64": - return finish(`scr_array_buffer_view_f64(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "arrayBuffer.viewDV": - return finish(`scr_array_buffer_view_dv(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.objKeys": - return finish(`scr_dyn_obj_keys(${arg(0)})`); - case "dyn.forInKeys": - return finish(`scr_dyn_for_in_keys(${arg(0)})`); - case "dyn.assign": - // Object.assign over dyn values: own members copy, the target - // returns (+1); non-object receivers throw like Node. - return finish(`scr_dyn_assign(${arg(0)}, ${arg(1)})`); - case "dyn.copyDataProperties": - return finish(`scr_dyn_copy_data_properties(${arg(0)}, ${arg(1)})`); - case "dyn.packPush": - // Variadic Object.assign's source pack: a plain source - // retains in (both args borrowed). Never throws. - return finish(`scr_dyn_pack_push(${arg(0)}, ${arg(1)})`); - case "dyn.packPushSpread": - // A spread source flattens through the spread-call walk — - // V8's exact TypeError texts (may-throw seed set); the string - // spells the spread expression for the nullish form. - return finish(`scr_dyn_pack_push_spread(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dyn.packPushSpreadIter": - // The non-last/multi-spread positions: V8's iterator-protocol - // failure texts describe the value (may-throw seed set). - return finish(`scr_dyn_pack_push_spread_iter(${arg(0)}, ${arg(1)})`); - case "dyn.assignAll": - // The flattened pack copies onto the target left to right; - // the target returns (+1). Nullish targets throw like Node. - return finish(`scr_dyn_assign_all(${arg(0)}, ${arg(1)})`); - case "dyn.objCreateNullProto": - // Object.create(null): the fresh null-prototype dictionary - // (+1). Never throws. - return finish(`scr_dyn_new_obj_null_proto()`); - case "dyn.objCreate": - return finish(`scr_dyn_obj_create(${arg(0)})`); - case "dyn.objCreateWithProperties": - return finish(`scr_dyn_obj_create_with_properties(${arg(0)}, ${arg(1)})`); - case "dyn.getPrototype": - return finish(`scr_dyn_get_prototype(${arg(0)})`); - case "dyn.setPrototype": - return finish(`scr_dyn_set_prototype(${arg(0)}, ${arg(1)})`); - case "dyn.getOwnPropertyNames": - return finish(`scr_dyn_get_own_property_names(${arg(0)})`); - case "dyn.getOwnPropertyDescriptors": - return finish(`scr_dyn_get_own_property_descriptors(${arg(0)})`); - case "dyn.preventExtensions": - return finish(`scr_dyn_prevent_extensions(${arg(0)})`); - case "dyn.isExtensible": - return finish(`scr_dyn_is_extensible(${arg(0)})`); - case "dyn.seal": - return finish(`scr_dyn_seal(${arg(0)})`); - case "dyn.isSealed": - return finish(`scr_dyn_is_sealed(${arg(0)})`); - case "dyn.hasOwn": - // Object.hasOwn over a dyn receiver (throws on nullish, like - // Node's ToObject). - return finish(`scr_dyn_has_own(${arg(0)}, ${arg(1)})`); - case "dyn.propertyIsEnumerable": - return finish(`scr_dyn_property_is_enumerable(${arg(0)}, ${arg(1)})`); - case "dyn.objValues": - return finish(`scr_dyn_obj_values(${arg(0)})`); - case "dyn.objEntries": - return finish(`scr_dyn_obj_entries(${arg(0)})`); - case "dyn.errInstanceof": - // The from_error cache resolves the dyn value to its runtime - // error; the class's stamped interval answers. Never throws. - return finish(`scr_dyn_err_instanceof(${arg(0)}, ${arg(1)})`); - case "dyn.structuredClone": - // Deep dyn clone (+1); option/DataClone/cycle errors throw - // (may-throw seed set). Both args borrowed. - return finish(`scr_structured_clone(${arg(0)}, ${arg(1)})`); - case "dyn.cloneMissing": - // Always throws ERR_MISSING_ARGS; the result never exists. - return finish(`scr_structured_clone_missing()`); - case "dyn.cloneTransferFail": - // Always throws DataCloneError; the result never exists. - return finish(`scr_structured_clone_transfer_fail()`); - default: - throw new InternalCompilerError(`emitter bug: dynamic libCall dispatch for ${fn}`); - } -} - -function emitFilesystemLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "fs.readFileSync": - // args[1] is the (always-"utf8") encoding: evaluated for - // JS-exact side-effect order, ignored by the runtime. - return finish(`scr_fs_read_file(${arg(0)})`); - case "fs.readFileSyncBuf": - return finish(`scr_fs_read_file_bytes(${arg(0)})`); - case "fs.readFileSyncDyn": - return finish(`scr_fs_read_file_sync_dyn(${arg(0)}, ${arg(1)})`); - case "fs.writeFileSync": - return finish(`scr_fs_write_file(${arg(0)}, ${arg(1)})`); - case "fs.appendFileSync": - return finish(`scr_fs_append_file(${arg(0)}, ${arg(1)})`); - case "fs.existsSync": - return finish(`scr_fs_exists(${arg(0)})`); - case "fs.mkdirSync": - return finish(`scr_fs_mkdir(${arg(0)})`); - case "fs.rmSync": - return finish(`scr_fs_rm(${arg(0)})`); - case "fs.rmdirSync": - return finish(`scr_fs_rmdir(${arg(0)})`); - case "fs.readdirSync": - return finish(`scr_fs_readdir(${arg(0)})`); - case "fs.readdirTypesSync": - case "fsp.readdirTypes": { - // Dirent rows assembled inline from one scandir snapshot - // (scr_lib.c) — the os.networkInterfaces pattern, flat. The - // frontend/validator pinned the shape ({%dtype, name, - // parentPath}); lookups here only guard emitter bugs. The sync - // form checks the snapshot's scandir error immediately. The - // promise form lets NULL flow through the null-tolerant count/ - // free accessors, builds an empty dummy array, and moves it to - // settled_ref: the pending exception becomes the rejection and - // settled_ref releases that dummy. - const promiseForm = e.fn === "fsp.readdirTypes"; - const arrayT = promiseForm && e.type.kind === "promise" ? e.type.inner : e.type; - if (arrayT.kind !== "array" || arrayT.elem.kind !== "record") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a Dirent record array`); - } - const recT = arrayT.elem; - const snap = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrScandir *${snap} = scr_fs_scandir(${arg(0)});${emitter.srcComment(e.loc)}`); - if (!promiseForm) emitter.emitPendingCheck(); - const out = emitter.newTemp(arrayT, emitter.arrNewC(recT, `scr_fs_scandir_count(${snap})`)); - const i = `sc_t${emitter.tempCounter++}`; - const n = `sc_t${emitter.tempCounter++}`; - emitter.line(`for (size_t ${i} = 0, ${n} = scr_fs_scandir_count(${snap}); ${i} < ${n}; ${i}++) {`); - emitter.indent++; - const row = `sc_t${emitter.tempCounter++}`; - emitter.line(`${mangleRecordStruct(recT.shapeId)} *${row} = ${mangleRecordNew(recT.shapeId)}();`); - emitter.line(`${row}->${mangleField("%dtype")} = scr_fs_scandir_type(${snap}, ${i});`); - emitter.line(`${row}->${mangleField("name")} = scr_fs_scandir_name(${snap}, ${i});`); - emitter.line(`${row}->${mangleField("parentPath")} = ${retainCallC({ kind: "string" }, arg(0))};`); - emitter.line(`scr_arr_push_ref(${out.name}, ${row});`); // push takes ownership of the row - emitter.indent--; - emitter.line(`}`); - emitter.line(`scr_fs_scandir_free(${snap});`); - if (promiseForm) { - const rc = vAdapters(arrayT); - emitter.moveTemp(out); // promise fulfillment owns the result array - return finish( - `scr_promise_settled_ref(${out.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arrayT)})`, - ); - } - return out; - } - // Stats (scr_lib.c): statSync throws like the other sync fs - // calls; the getters are pure reads. - case "fs.openSync": - // Throws Node-shaped fs errors (may-throw seed set); the fd - // comes back as f64. - return finish(`scr_fs_open(${arg(0)}, ${arg(1)})`); - case "fs.openNumericSync": - return finish(`scr_fs_open_numeric(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fs.fstatSync": - return finish(`scr_fs_fstat(${arg(0)})`); - case "fs.fchmodSync": - return finish(`scr_fs_fchmod(${arg(0)}, ${arg(1)})`); - case "fs.fsyncSync": - return finish(`scr_fs_fsync(${arg(0)})`); - case "fs.linkSync": - return finish(`scr_fs_link(${arg(0)}, ${arg(1)})`); - case "fs.readSync": - return finish(`scr_fs_read_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "fs.writeSync": - return finish(`scr_fs_write_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "fs.writeStrSync": - return finish(`scr_fs_write_str_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "fs.closeSync": - return finish(`scr_fs_close(${arg(0)})`); - case "fs.watch": - // Throws Node-shaped fs errors when the path won't open - // (may-throw seed set). An open watcher holds the loop: - // usesTimers. - emitter.usesTimers = true; - return finish(`scr_fs_watch(${arg(0)}, NULL, NULL)`); - case "fs.watchCb": { - // The callback MOVES into the watcher's registry; the adapter - // is runtime-provided per listener shape (zero-param, or the - // eventType string). - emitter.usesTimers = true; - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: fs.watchCb callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event"; - return finish(`scr_fs_watch(${arg(0)}, ${cb.name}, &${adapter})`); - } - case "watcher.close": - emitter.line(`scr_watcher_close(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "fs.statSync": - return finish(`scr_fs_stat(${arg(0)})`); - case "stats.isSymbolicLink": - return finish(`scr_stats_is_symlink(${arg(0)})`); - case "stats.blocks": - return finish(`scr_stats_blocks(${arg(0)})`); - case "stats.nlink": - return finish(`scr_stats_nlink(${arg(0)})`); - case "stats.atimeMs": - return finish(`scr_stats_atime_ms(${arg(0)})`); - case "stats.mtimeMs": - return finish(`scr_stats_mtime_ms(${arg(0)})`); - case "stats.ctimeMs": - return finish(`scr_stats_ctime_ms(${arg(0)})`); - case "stats.isFile": - return finish(`scr_stats_is_file(${arg(0)})`); - case "stats.isDirectory": - return finish(`scr_stats_is_dir(${arg(0)})`); - case "stats.size": - return finish(`scr_stats_size(${arg(0)})`); - case "stats.dev": - return finish(`scr_stats_dev(${arg(0)})`); - case "stats.ino": - return finish(`scr_stats_ino(${arg(0)})`); - case "fs.toUnixTimestamp": - return finish(`scr_fs_to_unix_timestamp(${arg(0)})`); - // The fs argument-validation ladders: the always-throw Chk - // forms (validation error or the trailing fence) take the - // error.nodeThrow dummy pattern; mkdtempSyncChk and the lchmod - // pair answer real results on a validated pass. - case "fs.existsChk": - emitter.usesTimers = true; // the scheduled answer holds the loop open - return finish(`scr_fs_exists_async(${arg(0)}, ${arg(1)})`); - case "fs.renameCb": { - // The callback MOVES into the scheduled operation. Its - // adapter constructs the program-specific Error | null union - // (or dyn error/null for the JS lane) when the timer fires. - emitter.usesTimers = true; - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: fs.rename callback not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = emitter.fsRenameThunkFor(cbT); - emitter.line(`scr_fs_rename_async(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "fs.mkdtempChk": - return finish( - `(scr_fs_mkdtemp_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.mkdtempSyncChk": - return finish(`scr_fs_mkdtemp_sync_chk(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fs.readFileChk": - return finish( - `(scr_fs_read_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.opendirChk": - return finish( - `(scr_fs_opendir_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.watchFileChk": - return finish( - `(scr_fs_watch_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.lchmodChk": - return finish( - `(scr_fs_lchmod_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.lchmodSyncChk": - return finish(`scr_fs_lchmod_sync_chk(${arg(0)}, ${arg(1)})`); - case "fsp.lchmodChk": - return finish(`scr_fsp_lchmod_chk(${arg(0)}, ${arg(1)})`); - case "fs.readChk": - return finish( - `(scr_fs_read_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "fs.streamOptsChk": - return finish( - `(scr_fs_stream_opts_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - // The fs Buffer forms (scr_bytes_io.c): the sync forms throw - // like the utf8 forms (may-throw seed set); the promise form - // rejects instead. - case "fs.readFileSyncBytes": - return finish(`scr_fs_read_file_bytes(${arg(0)})`); - case "fs.writeFileSyncBytes": - return finish(`scr_fs_write_file_bytes(${arg(0)}, ${arg(1)})`); - case "fs.appendFileSyncBytes": - return finish(`scr_fs_append_file_bytes(${arg(0)}, ${arg(1)})`); - case "fsp.readFileBytes": - return finish(`scr_fsp_read_file_bytes(${arg(0)})`); - // zlib (scr_zlib.c — linked only when these appear on the IR): - // inflate throws on corrupt input (may-throw seed set). - case "zlib.deflateSync": - return finish(`scr_zlib_deflate(${arg(0)})`); - case "zlib.inflateSync": - return finish(`scr_zlib_inflate(${arg(0)})`); - case "zlib.deflateRawSync": - return finish(`scr_zlib_deflate_mode(${arg(0)}, 1.0, -1.0)`); - case "zlib.deflateLevelSync": - return finish(`scr_zlib_deflate_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "zlib.inflateRawSync": - return finish(`scr_zlib_inflate_mode(${arg(0)}, 1.0)`); - case "zlib.gzipSync": - return finish(`scr_zlib_deflate_mode(${arg(0)}, 2.0, -1.0)`); - case "zlib.gunzipSync": - return finish(`scr_zlib_inflate_mode(${arg(0)}, 2.0)`); - case "zlib.unzipSync": - return finish(`scr_zlib_inflate_mode(${arg(0)}, 3.0)`); - case "zlib.crc32": - return finish(`scr_zlib_crc32(${arg(0)}, ${arg(1)})`); - case "zlib.deflateCb": - case "zlib.inflateCb": - case "zlib.deflateRawCb": - case "zlib.inflateRawCb": - case "zlib.gzipCb": - case "zlib.gunzipCb": - case "zlib.unzipCb": { - emitter.usesTimers = true; - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: zlib callback not a func"); - const callback = args[1]!; - emitter.moveTemp(callback); - const adapter = emitter.zlibBytesThunkFor(cbT); - const compressing = fn === "zlib.deflateCb" || fn === "zlib.deflateRawCb" || fn === "zlib.gzipCb"; - const mode = fn === "zlib.deflateRawCb" || fn === "zlib.inflateRawCb" - ? 1 - : fn === "zlib.gzipCb" || fn === "zlib.gunzipCb" - ? 2 - : fn === "zlib.unzipCb" - ? 3 - : 0; - emitter.line(`scr_zlib_codec_async(${arg(0)}, ${mode}.0, ${compressing ? "true" : "false"}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "fsp.readFile": - return finish(`scr_fsp_read_file(${arg(0)})`); - case "fsp.writeFile": - return finish(`scr_fsp_write_file(${arg(0)}, ${arg(1)})`); - case "fsp.writeFileMode": - return finish(`scr_fsp_write_file_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fsp.mkdir": - return finish(`scr_fsp_mkdir(${arg(0)})`); - case "fsp.mkdirMode": - return finish(`scr_fsp_mkdir_mode(${arg(0)}, ${arg(1)})`); - case "fsp.mkdirRecursive": - return finish(`scr_fsp_mkdir_recursive(${arg(0)})`); - case "fsp.mkdirRecursiveMode": - return finish(`scr_fsp_mkdir_recursive_mode(${arg(0)}, ${arg(1)})`); - case "fsp.unlink": - return finish(`scr_fsp_unlink(${arg(0)})`); - case "fsp.chmod": - return finish(`scr_fsp_chmod(${arg(0)}, ${arg(1)})`); - case "fsp.rename": - return finish(`scr_fsp_rename(${arg(0)}, ${arg(1)})`); - case "fsp.readdir": - return finish(`scr_fsp_readdir(${arg(0)})`); - case "fsp.rm": - return finish(`scr_fsp_rm(${arg(0)})`); - case "fsp.stat": - return finish(`scr_fsp_stat(${arg(0)})`); - case "fsp.realpath": - return finish(`scr_fsp_realpath(${arg(0)})`); - case "fsp.lstat": - return finish(`scr_fsp_lstat(${arg(0)})`); - case "fsp.open": - return finish(`scr_fsp_open(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fileHandle.fd": - return finish(`scr_file_handle_fd(${arg(0)})`); - case "fileHandle.close": - return finish(`scr_file_handle_close_promise(${arg(0)})`); - case "fileHandle.readFile": - return finish(`scr_file_handle_read_file_promise(${arg(0)}, ${arg(1)})`); - case "fileHandle.readFileBytes": - return finish(`scr_file_handle_read_file_bytes_promise(${arg(0)}, ${arg(1)})`); - case "fileHandle.writeFile": - return finish(`scr_file_handle_write_file_promise(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fileHandle.writeFileBytes": - return finish(`scr_file_handle_write_file_bytes_promise(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fileHandle.stat": - return finish(`scr_file_handle_stat_promise(${arg(0)})`); - case "fileHandle.read": - case "fileHandle.writeBytes": - case "fileHandle.writeStr": { - if (e.type.kind !== "promise" || e.type.inner.kind !== "record") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result`); - } - const inner = e.type.inner; - const countFn = e.fn === "fileHandle.read" - ? `scr_file_handle_read(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})` - : e.fn === "fileHandle.writeBytes" - ? `scr_file_handle_write_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})` - : `scr_file_handle_write_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`; - const count = emitter.newTemp(F64, countFn); - const row = emitter.newTemp(inner, `${mangleRecordNew(inner.shapeId)}()`); - const countField = e.fn === "fileHandle.read" ? "bytesRead" : "bytesWritten"; - emitter.line(`${row.name}->${mangleField(countField)} = ${count.name};`); - emitter.line(`${row.name}->${mangleField("buffer")} = ${retainCallC(e.args[1]!.type, arg(1))};`); - const rc = vAdapters(inner); - emitter.moveTemp(row); // promise fulfillment owns the result record - return finish(`scr_promise_settled_ref(${row.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(inner)})`); - } - case "fs.realpathSync": - return finish(`scr_fs_realpath(${arg(0)})`); - case "fs.realpathNativeSync": - return finish(`scr_fs_realpath_promise(${arg(0)})`); - // The fs option forms (scr_lib.c) — all in the may-throw seed, - // like the rest of sync fs. - case "fs.mkdirRecursiveSync": - return finish(`scr_fs_mkdir_recursive(${arg(0)})`); - case "fs.rmOptsSync": - return finish(`scr_fs_rm_opts(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fs.rmRetrySync": - return finish(`scr_fs_rm_opts_retry(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "fs.mkdtempSync": - return finish(`scr_fs_mkdtemp(${arg(0)})`); - case "fs.accessSync": - return finish(`scr_fs_access(${arg(0)}, ${arg(1)})`); - // The wider sync fs slice (scr_lib.c): syscall wrappers with - // Node's errno message shapes, `.code` stamped like the rest. - case "fs.unlinkSync": - return finish(`scr_fs_unlink(${arg(0)})`); - case "fs.chmodSync": - return finish(`scr_fs_chmod(${arg(0)}, ${arg(1)})`); - case "fs.chownSync": - return finish(`scr_fs_chown(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fs.copyFileSync": - return finish(`scr_fs_copyfile(${arg(0)}, ${arg(1)})`); - case "fs.renameSync": - return finish(`scr_fs_rename(${arg(0)}, ${arg(1)})`); - case "fs.lstatSync": - return finish(`scr_fs_lstat(${arg(0)})`); - case "fs.writeFileModeSync": - return finish(`scr_fs_write_file_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "fs.mkdirModeSync": - return finish(`scr_fs_mkdir_mode(${arg(0)}, ${arg(1)})`); - case "fs.mkdirRecursiveModeSync": - return finish(`scr_fs_mkdir_recursive_mode(${arg(0)}, ${arg(1)})`); - // Atomics.wait — the synchronous-sleep idiom (scr_lib.c): a - // real nanosleep; +1 string result; never throws. - case "atomics.wait": - return finish(`scr_atomics_wait(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "fs.readFdSync": - // args[1] is the (always-"utf8") encoding: evaluated for - // JS-exact side-effect order, ignored by the runtime. - return finish(`scr_fs_read_fd(${arg(0)})`); - case "fs.readFdSyncBytes": - return finish(`scr_fs_read_fd_bytes(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: filesystem libCall dispatch for ${fn}`); - } -} - -function emitPathUrlLibCall(state: LibCallState): Temp { - const { e, emitter, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:path (scr_path.c): pure string algorithms, POSIX rules. - // join/resolve borrow ONE packed string[] (the frontend built it); - // results are always +1 fresh strings. None of these throw. - case "path.join": - return finish(`scr_path_join(${arg(0)})`); - case "path.resolve": - return finish(`scr_path_resolve(${arg(0)})`); - case "path.normalize": - return finish(`scr_path_normalize(${arg(0)})`); - case "path.dirname": - return finish(`scr_path_dirname(${arg(0)})`); - case "path.basename": - return finish(`scr_path_basename(${arg(0)}, ${arg(1)})`); - case "path.extname": - return finish(`scr_path_extname(${arg(0)})`); - case "path.isAbsolute": - return finish(`scr_path_is_absolute(${arg(0)})`); - case "path.relative": - return finish(`scr_path_relative(${arg(0)}, ${arg(1)})`); - case "path.toNamespacedPath": - return finish(`scr_path_to_namespaced_path(${arg(0)})`); - // The win32 family (Node v24's path.win32 in scr_path.c): the - // path.win32 namespace anywhere, and the bare module's binding - // under a win32 target. - case "path.win32Join": - return finish(`scr_path_win32_join(${arg(0)})`); - case "path.win32Resolve": - return finish(`scr_path_win32_resolve(${arg(0)})`); - case "path.win32Normalize": - return finish(`scr_path_win32_normalize(${arg(0)})`); - case "path.win32Dirname": - return finish(`scr_path_win32_dirname(${arg(0)})`); - case "path.win32Basename": - return finish(`scr_path_win32_basename(${arg(0)}, ${arg(1)})`); - case "path.win32Extname": - return finish(`scr_path_win32_extname(${arg(0)})`); - case "path.win32IsAbsolute": - return finish(`scr_path_win32_is_absolute(${arg(0)})`); - case "path.win32Relative": - return finish(`scr_path_win32_relative(${arg(0)}, ${arg(1)})`); - case "path.win32ToNamespacedPath": - return finish(`scr_path_win32_to_namespaced_path(${arg(0)})`); - case "os.homedir": - return finish(`scr_os_homedir()`); - case "os.type": - return finish(`scr_os_type()`); - case "os.totalmem": - return finish(`scr_os_totalmem()`); - case "os.release": - return finish(`scr_os_release()`); - case "os.userName": - return finish(`scr_os_user_name()`); - case "os.userShell": - return finish(`scr_os_user_shell()`); - case "os.userHomedir": - return finish(`scr_os_user_homedir()`); - case "os.tmpdir": - return finish(`scr_os_tmpdir()`); - case "os.networkInterfaces": { - // The Dict record, built inline from a - // getifaddrs(3) snapshot (scr_lib.c) — the dgram.address - // pattern scaled up. Every shape/union/tag below comes from - // the call's own type; the frontend verified the structure - // (lowerOsNetworkInterfacesCall), so lookups here only guard - // against emitter bugs. Rows append to their interface's - // bucket in snapshot order; a first row makes the bucket (a - // fresh Info[] wrapped into the `Info[] | undefined` union arm - // the overflow map stores). - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: networkInterfaces result is not a record"); - const dictShape = emitter.recordsById.get(e.type.shapeId); - const iv = dictShape?.indexValue; - if (!dictShape || iv?.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces dict shape"); - const ivDef = emitter.unionsById.get(iv.unionId); - const arrTag = ivDef?.arms.findIndex((a) => a.kind === "array") ?? -1; - const arrT = ivDef?.arms[arrTag]; - if (arrT?.kind !== "array" || arrT.elem.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces bucket type"); - const infoT = arrT.elem; - const infoDef = emitter.unionsById.get(infoT.unionId); - if (!infoDef || infoDef.arms.length !== 2) throw new InternalCompilerError("emitter bug: networkInterfaces Info union"); - const tag6 = infoDef.arms.findIndex( - (a) => a.kind === "record" && emitter.recordsById.get(a.shapeId)?.fields.find((f) => f.name === "scopeid")?.type.kind === "f64", - ); - const tag4 = 1 - tag6; - const armShape = (tag: number): { t: IrType & { kind: "record" }; shape: IrRecordShape } => { - const t = infoDef.arms[tag]; - if (t?.kind !== "record") throw new InternalCompilerError("emitter bug: networkInterfaces Info arm"); - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) throw new InternalCompilerError("emitter bug: networkInterfaces Info shape"); - return { t, shape }; - }; - const dict = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - const snap = `sc_t${emitter.tempCounter++}`; - const i = `sc_t${emitter.tempCounter++}`; - const n = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrIfaddrs *${snap} = scr_os_ifaddrs();${emitter.srcComment(e.loc)}`); - emitter.line(`for (size_t ${i} = 0, ${n} = scr_os_ifaddrs_count(${snap}); ${i} < ${n}; ${i}++) {`); - emitter.indent++; - const row = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrUnion *${row};`); - const emitRow = (tag: number, v6: boolean): void => { - const { t, shape } = armShape(tag); - const cidrT = shape.fields.find((f) => f.name === "cidr")?.type; - const cidrDef = cidrT?.kind === "union" ? emitter.unionsById.get(cidrT.unionId) : undefined; - if (cidrT?.kind !== "union" || !cidrDef) throw new InternalCompilerError("emitter bug: networkInterfaces cidr type"); - const cidrStrTag = cidrDef.arms.findIndex((a) => a.kind === "string"); - const cidrNullTag = cidrDef.arms.findIndex((a) => a.kind === "nullT"); - const r = `sc_t${emitter.tempCounter++}`; - const struct = mangleRecordStruct(t.shapeId); - emitter.line(`${struct} *${r} = ${mangleRecordNew(t.shapeId)}();`); - emitter.line(`${r}->${mangleField("address")} = scr_os_ifaddrs_address(${snap}, ${i});`); - emitter.line(`${r}->${mangleField("netmask")} = scr_os_ifaddrs_netmask(${snap}, ${i});`); - emitter.line(`${r}->${mangleField("family")} = scr_os_ifaddrs_family(${snap}, ${i});`); - emitter.line(`${r}->${mangleField("mac")} = scr_os_ifaddrs_mac(${snap}, ${i});`); - emitter.line(`${r}->${mangleField("internal")} = scr_os_ifaddrs_internal(${snap}, ${i});`); - const cs = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrStr *${cs} = scr_os_ifaddrs_cidr(${snap}, ${i});`); - emitter.line( - `${r}->${mangleField("cidr")} = ${cs} ? scr_union_new_ref(${cidrStrTag}, ${cs}, &scr_str_retain_v, &scr_str_release_v, NULL) : scr_union_retain(${emitter.unitInstanceRef(cidrT.unionId, cidrNullTag)});`, - ); - if (v6) { - emitter.line(`${r}->${mangleField("scopeid")} = scr_os_ifaddrs_scopeid(${snap}, ${i});`); - } else { - const st = shape.fields.find((f) => f.name === "scopeid")?.type; - if (st?.kind !== "union") throw new InternalCompilerError("emitter bug: networkInterfaces IPv4 scopeid type"); - const undefTag = undefinedArmTag(st, emitter.unionsById); - emitter.line(`${r}->${mangleField("scopeid")} = scr_union_retain(${emitter.unitInstanceRef(st.unionId, undefTag)});`); - } - const rc = vAdapters(t); - emitter.line(`${row} = scr_union_new_ref(${tag}, ${r}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});`); - }; - emitter.line(`if (scr_os_ifaddrs_ipv6(${snap}, ${i})) {`); - emitter.indent++; - emitRow(tag6, true); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitRow(tag4, false); - emitter.indent--; - emitter.line(`}`); - const name = `sc_t${emitter.tempCounter++}`; - const cell = `sc_t${emitter.tempCounter++}`; - const rows = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrStr *${name} = scr_os_ifaddrs_name(${snap}, ${i});`); - emitter.line(`ScrUnion *${cell} = scr_map_get_str_ref(${dict.name}->${OVERFLOW_MEMBER}, ${name});`); - emitter.line(`ScrArr *${rows};`); - emitter.line(`if (${cell}) {`); - emitter.indent++; - emitter.line(`${rows} = scr_arr_retain((ScrArr *)scr_union_peek(${cell}));`); - emitter.line(`scr_union_release(${cell});`); - emitter.indent--; - emitter.line(`} else {`); - emitter.indent++; - emitter.line(`${rows} = ${emitter.arrNewC(infoT, 1)};`); - const arrRc = vAdapters(arrT); - emitter.line( - `scr_map_set_str_ref(${dict.name}->${OVERFLOW_MEMBER}, ${name}, scr_union_new_ref(${arrTag}, scr_arr_retain(${rows}), &${arrRc.retain}, &${arrRc.release}, ${emitter.traceArgC(arrT)}));`, - ); - emitter.indent--; - emitter.line(`}`); - emitter.line(`scr_arr_push_ref(${rows}, ${row});`); // push takes ownership of the row - emitter.line(`scr_arr_release(${rows});`); - emitter.line(`scr_str_release(${name});`); - emitter.indent--; - emitter.line(`}`); - emitter.line(`scr_os_ifaddrs_free(${snap});`); - return dict; - } - case "url.new": - return finish(`scr_url_new(${arg(0)})`); - case "url.newBase": - return finish(`scr_url_new_base(${arg(0)}, ${arg(1)})`); - case "url.protocol": - return finish(`scr_url_protocol(${arg(0)})`); - case "url.origin": - return finish(`scr_url_origin(${arg(0)})`); - case "url.username": - return finish(`scr_url_username(${arg(0)})`); - case "url.password": - return finish(`scr_url_password(${arg(0)})`); - case "url.host": - return finish(`scr_url_host(${arg(0)})`); - case "url.hostname": - return finish(`scr_url_hostname(${arg(0)})`); - case "url.port": - return finish(`scr_url_port(${arg(0)})`); - case "url.pathname": - return finish(`scr_url_pathname(${arg(0)})`); - case "url.href": - return finish(`scr_url_href(${arg(0)})`); - case "url.fileURLToPathUrl": - return finish(`scr_url_to_path(${arg(0)})`); - case "url.fileURLToPathChecked": return finish(`scr_url_checked_to_path(${arg(0)})`); - case "url.fileURLToPathStr": - return finish(`scr_url_str_to_path(${arg(0)})`); - case "url.pathToFileURL": - // The win32-target flavor is the SAME entry point (the bridge - // dispatches by _WIN32) — the distinct name exists for the - // may-throw seed (the win32 arm's UNC TypeErrors). - case "url.pathToFileURLWin32": - return finish(`scr_url_from_path(${arg(0)})`); - case "url.pathToFileURLPlatform": - return finish(`scr_url_from_path_platform(${arg(0)}, ${arg(1)})`); - // URLSearchParams (scr_url.c — always linked with the url unit). - // Constructions come back +1; sp.fromPairs throws Node's - // ERR_INVALID_TUPLE catchably (may-throw seed set). Mutators are - // void borrowed-receiver calls; a live view's URL query updates - // inside the runtime. - case "sp.new": - return finish(`scr_sp_new()`); - case "sp.parse": - return finish(`scr_sp_parse(${arg(0)})`); - case "sp.copy": - return finish(`scr_sp_copy(${arg(0)})`); - case "sp.fromPairs": - return finish(`scr_sp_from_pairs(${arg(0)})`); - case "sp.with": - return finish(`scr_sp_with(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "url.searchParams": - return finish(`scr_url_search_params(${arg(0)})`); - case "url.search": - return finish(`scr_url_search(${arg(0)})`); - case "url.hash": - return finish(`scr_url_hash(${arg(0)})`); - case "sp.get": { - // `string | null` — the sym.desc pattern with a null arm: the - // runtime answers a +1 string or NULL. - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: sp.get result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (strTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: sp.get union lacks its arms"); - } - const raw = emitter.newTemp(STRING, `scr_sp_get(${arg(0)}, ${arg(1)})`); - emitter.moveTemp(raw); // ownership passes into the union arm below - const present = `scr_union_new_ref(${strTag}, ${raw.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`); - } - case "sp.getAll": - return finish(`scr_sp_get_all(${arg(0)}, ${arg(1)})`); - case "sp.append": - return finish(`scr_sp_append(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "sp.set": - return finish(`scr_sp_set(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "sp.delete": - return finish(`scr_sp_delete(${arg(0)}, ${arg(1)})`); - case "sp.deleteValue": - return finish(`scr_sp_delete_value(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "sp.has": - return finish(`scr_sp_has(${arg(0)}, ${arg(1)})`); - case "sp.hasValue": - return finish(`scr_sp_has_value(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "sp.sort": - return finish(`scr_sp_sort(${arg(0)})`); - case "sp.size": - return finish(`scr_sp_size(${arg(0)})`); - case "sp.toString": - return finish(`scr_sp_to_string(${arg(0)})`); - case "sp.keyAt": - return finish(`scr_sp_key_at(${arg(0)}, ${arg(1)})`); - case "sp.valAt": - return finish(`scr_sp_val_at(${arg(0)}, ${arg(1)})`); - // node:querystring (scr_qs.c — linked exactly when parse/ - // stringify/unescape appear, moduleUsesQs). escape IS the - // component encoder (Node's qsEscape set equals - // encodeURIComponent's), so it emits the always-linked codec - // and never pulls the unit. Borrow; string results +1; no throw. - case "qs.escape": - return finish(`scr_str_encode_uri_component(${arg(0)})`); - case "qs.unescape": - return finish(`scr_qs_unescape(${arg(0)})`); - case "qs.stringify": - return finish(`scr_qs_stringify(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "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 frontend verified the shape (lowerQuerystringParseCall); - // lookups here only guard emitter bugs. Args: qs, sep, eq, - // maxKeys. - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: qs.parse result is not a record"); - const dictShape = emitter.recordsById.get(e.type.shapeId); - const iv = dictShape?.indexValue; - if (!dictShape || iv?.kind !== "union") throw new InternalCompilerError("emitter bug: qs.parse dict shape"); - const ivDef = emitter.unionsById.get(iv.unionId); - const strTag = ivDef?.arms.findIndex((a) => a.kind === "string") ?? -1; - const arrTag = ivDef?.arms.findIndex((a) => a.kind === "array") ?? -1; - if (strTag < 0 || arrTag < 0) throw new InternalCompilerError("emitter bug: qs.parse index union lacks its arms"); - const dict = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - emitter.line( - `scr_qs_parse_into(${dict.name}->${OVERFLOW_MEMBER}, ${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${strTag}, ${arrTag});${emitter.srcComment(e.loc)}`, - ); - return dict; - } - case "sp.finished": - // The stream/promises form: a +1 pending promise the terminal - // watcher settles — no callback, no cleanup exposure. - emitter.usesTimers = true; - return finish(`scr_sp_finished((ScrStream *)${arg(0)})`); - case "sp.pipeline": { - // pipeline(count, s1..sn) settling a void promise; the stream - // list rides the callback form's compound literal. - emitter.usesTimers = true; - const countArg = e.args[0]!; - if (countArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} count not a literal`); - const n = countArg.value; - const list = Array.from({ length: n }, (_, i) => `(ScrStream *)${arg(1 + i)}`).join(", "); - return finish(`scr_sp_pipeline(${n}, (ScrStream *[]){ ${list} })`); - } - default: - throw new InternalCompilerError(`emitter bug: pathUrl libCall dispatch for ${fn}`); - } -} - -function emitPrimitiveLibCall(state: LibCallState): Temp { - const { e, emitter, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // Math.max/min over one spread number[] — the JS fold in C - // (NaN poisons, ±0 order, empty → ∓Infinity). Borrows; no throw. - case "math.maxArr": - return finish(`scr_math_max_arr(${arg(0)})`); - case "math.minArr": - return finish(`scr_math_min_arr(${arg(0)})`); - case "math.hypotArr": - return finish(`scr_math_hypot_arr(${arg(0)})`); - // Math.floor/trunc/ceil — the C functions ARE the JS operations - // (math.h is always included). Borrow nothing; no throw. - case "math.floor": - return finish(`floor(${arg(0)})`); - case "math.trunc": - return finish(`trunc(${arg(0)})`); - case "math.ceil": - return finish(`ceil(${arg(0)})`); - case "math.sin": - return finish(`sin(${arg(0)})`); - case "math.sinh": - return finish(`sinh(${arg(0)})`); - case "math.cos": - return finish(`cos(${arg(0)})`); - case "math.cosh": - return finish(`cosh(${arg(0)})`); - case "math.tan": - return finish(`tan(${arg(0)})`); - case "math.tanh": - return finish(`tanh(${arg(0)})`); - case "math.asin": - return finish(`asin(${arg(0)})`); - case "math.asinh": - return finish(`asinh(${arg(0)})`); - case "math.acos": - return finish(`acos(${arg(0)})`); - case "math.acosh": - return finish(`acosh(${arg(0)})`); - case "math.atan": - return finish(`atan(${arg(0)})`); - case "math.atanh": - return finish(`atanh(${arg(0)})`); - case "math.cbrt": - return finish(`cbrt(${arg(0)})`); - case "math.clz32": - return finish(`scr_math_clz32(${arg(0)})`); - case "math.sign": - // Each argument was evaluated into a temp. Returning that temp - // for unordered/zero inputs preserves NaN and the sign of zero. - return finish(`(${arg(0)} > 0.0 ? 1.0 : (${arg(0)} < 0.0 ? -1.0 : ${arg(0)}))`); - case "math.exp": - return finish(`exp(${arg(0)})`); - case "math.expm1": - return finish(`expm1(${arg(0)})`); - case "math.fround": - return finish(`scr_math_fround(${arg(0)})`); - case "math.sqrt": - return finish(`sqrt(${arg(0)})`); - case "math.log": - return finish(`log(${arg(0)})`); - case "math.log1p": - return finish(`log1p(${arg(0)})`); - case "math.log2": - return finish(`log2(${arg(0)})`); - case "math.log10": - return finish(`log10(${arg(0)})`); - case "math.atan2": - return finish(`atan2(${arg(0)}, ${arg(1)})`); - case "math.pow": - return finish(`scr_math_pow(${arg(0)}, ${arg(1)})`); - case "math.imul": - return finish(`scr_math_imul(${arg(0)}, ${arg(1)})`); - // Math.abs — C fabs IS the JS operation. Math.round — the JS - // half-toward-+Infinity rule (scr_lib.c; C round() differs on - // halves and naive floor(x+0.5) drifts at the epsilon boundary). - case "math.abs": - return finish(`fabs(${arg(0)})`); - case "math.round": - return finish(`scr_math_round(${arg(0)})`); - // The scalar Math.min/max (scr_lib.c — fmin/fmax drop NaN, so - // these are the JS folds) and Math.random (arc4random-backed - // uniform [0,1), SEMANTICS.md 62). Borrow nothing; no throw. - case "math.min": - return finish(`scr_math_min(${arg(0)}, ${arg(1)})`); - case "math.max": - return finish(`scr_math_max(${arg(0)}, ${arg(1)})`); - case "math.random": - return finish(`scr_math_random()`); - // The static global parsers/tests (scr_string.c). Borrow; no throw. - case "num.parseInt": - return finish(`scr_parse_int(${arg(0)}, ${arg(1)})`); - case "num.parseFloat": - return finish(`scr_parse_float(${arg(0)})`); - case "num.fromString": - return finish(`scr_string_to_number(${arg(0)})`); - case "num.isNaN": - return finish(`(bool)isnan(${arg(0)})`); - // The URI codecs (scr_string.c). Borrow; results +1. decode - // throws the spec's catchable URIError on bad hex or invalid - // UTF-8 octets (may-throw seed set); the encoders never throw. - case "str.encodeUriComponent": - return finish(`scr_str_encode_uri_component(${arg(0)})`); - case "str.decodeUriComponent": - return finish(`scr_str_decode_uri_component(${arg(0)})`); - case "str.encodeUri": - return finish(`scr_encode_uri(${arg(0)})`); - // RegExp.escape (scr_string.c): total per-code-point escape — - // borrows, +1 result, never throws. - case "regexp.escape": - return finish(`scr_regexp_escape(${arg(0)})`); - // The WHATWG base64 globals (scr_string.c): dyn arg borrowed - // (WebIDL ToString in the runtime), +1 string result; malformed - // input throws the catchable DOMException InvalidCharacterError - // (may-throw seed set). The zero-argument form always throws - // Node's TypeError [ERR_MISSING_ARGS]. - case "str.atob": - return finish(`scr_atob(${arg(0)})`); - case "str.btoa": - return finish(`scr_btoa(${arg(0)})`); - case "str.b64Missing": - return finish(`scr_b64_missing_arg()`); - // Number.prototype formatters (scr_lib.c). Borrow nothing; - // successful results +1; explicit toFixed may throw RangeError. - case "num.toExponential": - return finish(`scr_num_to_exponential(${arg(0)})`); - case "num.toFixed0": - return finish(`scr_num_to_fixed0(${arg(0)})`); - case "num.toFixed": - return finish(`scr_num_to_fixed(${arg(0)}, ${arg(1)})`); - // Object.is over two numbers — SameValue on doubles. No throw. - case "num.sameValue": - return finish(`scr_num_same_value(${arg(0)}, ${arg(1)})`); - // Intl.NumberFormat("en-US").format / toLocaleString("en-US") - // with default options (scr_lib.c). +1 string; no throw. - case "intl.segmenterNew": - return finish(`scr_intl_segmenter_new()`); - case "intl.numFormatEnUs": - return finish(`scr_intl_num_format_en_us(${arg(0)})`); - // The URL surface (scr_url.c): construction and the two - // fileURLToPath receiver forms throw catchable TypeErrors - // (may-throw seed set); the getters are pure reads. Receivers - // and string args are borrowed; every result is +1. - // ES symbols (scr_symbol.c — linked exactly when these appear, - // moduleUsesSymbol). Fresh identities (+1); Symbol.for answers - // +1 on the registry's interned per-key symbol. None throw. - case "sym.new": - return finish(`scr_sym_new(${arg(0)})`); - case "sym.newAnon": - return finish(`scr_sym_new(NULL)`); - case "sym.for": - return finish(`scr_sym_for(${arg(0)})`); - case "sym.toString": - return finish(`scr_sym_to_string(${arg(0)})`); - case "bigint.parse": - return finish(`scr_bigint_parse(${arg(0)})`); - case "bigint.fromF64": - return finish(`scr_bigint_from_f64(${arg(0)})`); - case "bigint.neg": - return finish(`scr_bigint_neg(${arg(0)})`); - case "bigint.not": - return finish(`scr_bigint_not(${arg(0)})`); - case "bigint.add": - return finish(`scr_bigint_add(${arg(0)}, ${arg(1)})`); - case "bigint.sub": - return finish(`scr_bigint_sub(${arg(0)}, ${arg(1)})`); - case "bigint.mul": - return finish(`scr_bigint_mul(${arg(0)}, ${arg(1)})`); - case "bigint.div": - return finish(`scr_bigint_div(${arg(0)}, ${arg(1)})`); - case "bigint.mod": - return finish(`scr_bigint_mod(${arg(0)}, ${arg(1)})`); - case "bigint.pow": - return finish(`scr_bigint_pow(${arg(0)}, ${arg(1)})`); - case "bigint.and": - return finish(`scr_bigint_and(${arg(0)}, ${arg(1)})`); - case "bigint.or": - return finish(`scr_bigint_or(${arg(0)}, ${arg(1)})`); - case "bigint.xor": - return finish(`scr_bigint_xor(${arg(0)}, ${arg(1)})`); - case "bigint.shl": - return finish(`scr_bigint_shl(${arg(0)}, ${arg(1)})`); - case "bigint.shr": - return finish(`scr_bigint_shr(${arg(0)}, ${arg(1)})`); - case "bigint.eq": - return finish(`scr_bigint_eq(${arg(0)}, ${arg(1)})`); - case "bigint.eqString": - return finish(`scr_bigint_eq_string(${arg(0)}, ${arg(1)})`); - case "bigint.cmp": - return finish(`scr_bigint_cmp_f64(${arg(0)}, ${arg(1)})`); - case "bigint.cmpNumber": - return finish(`scr_bigint_cmp_number(${arg(0)}, ${arg(1)})`); - case "bigint.truthy": - return finish(`scr_bigint_truthy(${arg(0)})`); - case "bigint.toString": - return finish(`scr_bigint_to_string(${arg(0)}, ${arg(1)})`); - case "bigint.inspect": - return finish(`scr_bigint_inspect(${arg(0)})`); - case "bigint.toF64": - return finish(`scr_bigint_to_f64(${arg(0)})`); - case "bigint.asUintN": - return finish(`scr_bigint_as_uint_n(${arg(0)}, ${arg(1)})`); - case "bigint.asIntN": - return finish(`scr_bigint_as_int_n(${arg(0)}, ${arg(1)})`); - case "bigint.bufferRead": - return finish(`scr_bigint_buffer_read(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "bigint.bufferWrite": - return finish(`scr_bigint_buffer_write(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "bigint.dataViewGet": - return finish(`scr_bigint_dataview_get(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "bigint.dataViewSet": - return finish(`scr_bigint_dataview_set(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "sym.desc": - case "sym.keyFor": { - // `string | undefined` — the child.stdout pattern with a - // string arm: the runtime answers a +1 string or NULL. - if (e.type.kind !== "union") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`); - } - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1; - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`); - } - const get = e.fn === "sym.desc" ? "scr_sym_desc" : "scr_sym_key_for"; - const raw = emitter.newTemp(STRING, `${get}(${arg(0)})`); - emitter.moveTemp(raw); // ownership passes into the union arm below - const present = `scr_union_new_ref(${strTag}, ${raw.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`); - } - case "perf.now": - return finish(`scr_perf_now()`); - // The Number statics (scr_lib.c): pure f64 → bool, never throw. - case "number.isFinite": - return finish(`scr_num_is_finite(${arg(0)})`); - case "number.isNaN": - return finish(`scr_num_is_nan(${arg(0)})`); - case "number.isInteger": - return finish(`scr_num_is_integer(${arg(0)})`); - case "number.isSafeInteger": - return finish(`scr_num_is_safe_integer(${arg(0)})`); - case "date.now": - // Node's integer milliseconds since epoch. Never throws. - return finish(`scr_date_now()`); - case "date.newNow": - return finish(`scr_date_now()`); - case "date.newMs": - return finish(`scr_date_new_ms(${arg(0)})`); - case "date.newString": - return finish(`scr_date_parse_get_time(${arg(0)})`); - case "date.getTime": - case "date.valueOf": - return finish(`${arg(0)}`); - case "date.toISOString": - case "date.toISOStringValue": - // +1 string, or Node's "Invalid time value" RangeError - // (may-throw seed set). - return finish(`scr_date_to_iso(${arg(0)})`); - case "date.parse": - case "date.parseGetTime": - // The bounded date-string parse (X509 validity + ECMA format); - // NaN elsewhere. Never throws. - return finish(`scr_date_parse_get_time(${arg(0)})`); - case "date.utc": - // MakeDay/MakeTime/TimeClip over seven completed number - // arguments; NaN outside the time range. Never throws. - return finish( - `scr_date_utc(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`, - ); - case "date.getFullYear": - return finish(`scr_date_get_full_year(${arg(0)}, false)`); - case "date.getUTCFullYear": - return finish(`scr_date_get_full_year(${arg(0)}, true)`); - case "date.getMonth": - return finish(`scr_date_get_month(${arg(0)}, false)`); - case "date.getUTCMonth": - return finish(`scr_date_get_month(${arg(0)}, true)`); - case "date.getDate": - return finish(`scr_date_get_date(${arg(0)}, false)`); - case "date.getUTCDate": - return finish(`scr_date_get_date(${arg(0)}, true)`); - case "date.getDay": - return finish(`scr_date_get_day(${arg(0)}, false)`); - case "date.getUTCDay": - return finish(`scr_date_get_day(${arg(0)}, true)`); - case "date.getHours": - return finish(`scr_date_get_hours(${arg(0)}, false)`); - case "date.getUTCHours": - return finish(`scr_date_get_hours(${arg(0)}, true)`); - case "date.getMinutes": - return finish(`scr_date_get_minutes(${arg(0)}, false)`); - case "date.getUTCMinutes": - return finish(`scr_date_get_minutes(${arg(0)}, true)`); - case "date.getSeconds": - return finish(`scr_date_get_seconds(${arg(0)}, false)`); - case "date.getUTCSeconds": - return finish(`scr_date_get_seconds(${arg(0)}, true)`); - case "date.getMilliseconds": - case "date.getUTCMilliseconds": - return finish(`scr_date_get_milliseconds(${arg(0)})`); - case "date.getTimezoneOffset": - return finish(`scr_date_get_timezone_offset(${arg(0)})`); - case "text.decode": - // WHATWG utf-8 decode with the leading BOM stripped - // (scr_bytes.c). Borrowed bytes; +1 string; never throws. - return finish(`scr_text_decode(${arg(0)})`); - case "text.decodeLegacy": - // Compile-time-labeled WHATWG legacy decode (scr_bytes.c). - // Borrowed bytes + numeric encoding id; +1 string. - return finish(`scr_text_decode_legacy(${arg(0)}, ${arg(1)})`); - case "string.fromCharCode": - // One packed f64[] (the frontend built it) or one bytes value - // (the spread-typed-array form); +1 string. - return finish( - e.args[0]!.type.kind === "bytes" - ? `scr_str_from_char_code_bytes(${arg(0)})` - : `scr_str_from_char_code(${arg(0)})`, - ); - case "string.fromCodePoint": - return finish(e.args[0]!.type.kind === "bytes" - ? `scr_str_from_code_point_bytes(${arg(0)})` - : `scr_str_from_code_point(${arg(0)})`); - case "string.lastIndexOf": - return finish(`scr_str_last_index_of(${arg(0)}, ${arg(1)})`); - case "string.lastIndexOfFrom": - return finish(`scr_str_last_index_of_from(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "string.raw": - // Raw literals + pre-stringified substitutions; +1 string. - return finish(`scr_str_raw(${arg(0)}, ${arg(1)})`); - case "class.name": - // `X.name` through a class value: the class object's stored - // .name string (+1 — a no-op retain on the interned immortal). - return finish(`scr_classobj_name(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: primitive libCall dispatch for ${fn}`); - } -} - -function emitUtilLibCall(state: LibCallState): Temp { - const { e, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:util.parseArgs (scr_util.c): checked-dynamic config in, - // checked-dynamic result out; may throw a coded TypeError. - case "util.parseArgs": - return finish(`scr_util_parse_args(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: util libCall dispatch for ${fn}`); - } -} - -function emitCryptoBytesLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "crypto.x509Fingerprint": - // Throws Node's PEM error on unparseable input (may-throw). - return finish(`scr_crypto_x509_fingerprint(${arg(0)})`); - case "crypto.x509FingerprintStr": - return finish(`scr_crypto_x509_fingerprint_str(${arg(0)})`); - case "crypto.x509ValidFrom": - // The Validity walk shares the fingerprint's PEM contract - // (may-throw); the string is Node's ASN1_TIME_print shape. - return finish(`scr_crypto_x509_valid_from(${arg(0)})`); - case "crypto.x509ValidFromStr": - return finish(`scr_crypto_x509_valid_from_str(${arg(0)})`); - case "crypto.x509ValidTo": - return finish(`scr_crypto_x509_valid_to(${arg(0)})`); - case "crypto.x509ValidToStr": - return finish(`scr_crypto_x509_valid_to_str(${arg(0)})`); - // fs/promises: already-settled promises — failures REJECT (the - // runtime moves the pending exception into the promise), so no - // pending check here; the await re-throws catchably. args[1] of - // readFile is the (always-"utf8") encoding, evaluated for - // JS-exact side-effect order and ignored by the runtime. - case "crypto.randomUUID": - return finish(`scr_crypto_random_uuid()`); - case "crypto.randomBytesToString": - // May throw Node's RangeError (may-throw seed set). - return finish(`scr_crypto_random_string(${arg(0)}, ${arg(1)})`); - case "crypto.randomBytes": - // A real u8 Buffer (+1); same RangeError as the composed form. - return finish(`scr_crypto_random_bytes(${arg(0)})`); - case "crypto.hashDigestStr": - return finish(`scr_crypto_hash_digest_str(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "crypto.hashDigestBytes": - return finish(`scr_crypto_hash_digest_bytes(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "crypto.hashNew": - return finish(`scr_crypto_hash_new(${arg(0)})`); - case "crypto.hmacNewStr": - return finish(`scr_crypto_hmac_new_str(${arg(0)}, ${arg(1)})`); - case "crypto.hmacNewBytes": - return finish(`scr_crypto_hmac_new_bytes(${arg(0)}, ${arg(1)})`); - case "crypto.hashUpdateStr": - case "crypto.hmacUpdateStr": - return finish(`scr_crypto_hash_update_str(${arg(0)}, ${arg(1)})`); - case "crypto.hashUpdateBytes": - case "crypto.hmacUpdateBytes": - return finish(`scr_crypto_hash_update_bytes(${arg(0)}, ${arg(1)})`); - case "crypto.hashCopy": - return finish(`scr_crypto_hash_copy(${arg(0)})`); - case "crypto.hashDigestString": - case "crypto.hmacDigestString": - return finish(`scr_crypto_hash_digest_string(${arg(0)}, ${arg(1)})`); - case "crypto.hashDigestBuffer": - case "crypto.hmacDigestBuffer": - return finish(`scr_crypto_hash_digest_buffer(${arg(0)})`); - case "crypto.timingSafeEqual": - return finish(`scr_crypto_timing_safe_equal(${arg(0)}, ${arg(1)})`); - case "crypto.randomFill": - return finish(`scr_crypto_random_fill(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "crypto.randomFillRest": - return finish(`scr_crypto_random_fill_rest(${arg(0)}, ${arg(1)})`); - case "crypto.randomInt": - return finish(`scr_crypto_random_int(${arg(0)}, ${arg(1)})`); - case "crypto.pbkdf2": - return finish(`scr_crypto_pbkdf2(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "crypto.randomBytesCb": - case "crypto.pbkdf2Cb": { - emitter.usesTimers = true; - const cbIndex = fn === "crypto.randomBytesCb" ? 1 : 5; - const cbT = e.args[cbIndex]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: crypto callback not a func"); - const callback = args[cbIndex]!; - emitter.moveTemp(callback); - const adapter = emitter.cryptoBytesThunkFor(cbT); - if (fn === "crypto.randomBytesCb") { - emitter.line(`scr_crypto_random_bytes_async(${arg(0)}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`); - } else { - emitter.line(`scr_crypto_pbkdf2_async(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${callback.name}, &${adapter});${emitter.srcComment(e.loc)}`); - } - if (MAY_THROW_LIB_FNS.has(fn)) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - // The Buffer statics (scr_bytes.c): fromStr decodes Node- - // leniently (never throws), concat copies its borrowed list. - case "buffer.fromStr": - return finish(`scr_bytes_from_str(${arg(0)}, ${arg(1)})`); - case "buffer.brand": - return finish(`scr_bytes_as_buffer(${arg(0)})`); - case "buffer.fromDyn": - return finish(`scr_buffer_from_dyn(${arg(0)}, ${arg(1)})`); - case "buffer.concat": - return finish(`scr_bytes_concat(${arg(0)})`); - case "buffer.concatLen": - return finish(`scr_bytes_concat_len(${arg(0)}, ${arg(1)})`); - case "buffer.byteLenStr": - return finish(`scr_bytes_byte_length_str(${arg(0)}, ${arg(1)})`); - case "buffer.isEncoding": - return finish(`scr_bytes_is_encoding(${arg(0)})`); - // The checked-dynamic compare/equals validators - // (scr_bytes_io.c): Node's argument ladders throw catchably - // (may-throw seed set); all dyn args borrowed. - case "buffer.compareChk": - return finish(`scr_buffer_compare_chk(${arg(0)}, ${arg(1)})`); - case "bytes.equalsChk": - return finish(`scr_bytes_equals_chk(${arg(0)}, ${arg(1)})`); - case "bytes.compareChk": - return finish(`scr_bytes_compare_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "buffer.newStringFail": - return finish(`scr_buffer_new_string_fail(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: cryptoBytes libCall dispatch for ${fn}`); - } -} - -function emitChildProcessLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // child_process.spawnSync (scr_child.c): blocks until the child - // is reaped — NEVER throws (spawn failure is data: status null, - // empty outputs). cmd and args are borrowed; the result is +1. - case "cp.spawnSync": - return finish(`scr_spawn_sync(${arg(0)}, ${arg(1)})`); - case "cp.spawnSyncOpts": - // The options form: timeout/killSignal/stdio modes. Same - // never-throws contract — timeout and spawn failure are data - // on the result (error/signal), exactly Node's spawnSync. - return finish( - `scr_spawn_sync_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`, - ); - case "cp.spawnSyncStdioStr": - // The runtime maps the type-proven "pipe"/"ignore"/"inherit" - // string to the three modes; same never-throws contract. - return finish( - `scr_spawn_sync_stdio_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`, - ); - case "cp.execSync": - // Throws Node's exec errors; the +1 stdout result must join - // the frame BEFORE the pending check (fallibleTemp) so an - // unwind releases the NULL dummy harmlessly. - return emitter.fallibleTemp( - e.type, - `scr_exec_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, ${arg(9)}, ${arg(10)})`, - ); - case "cp.execCapture": - // The promisified-execFile core: throws Node's ASYNC exec - // errors (the interned async helper's fiber makes them the - // rejection); +1 spawnRes result, same fallible discipline. - return emitter.fallibleTemp( - e.type, - `scr_exec_capture(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`, - ); - case "spawnRes.status": { - // The `number | null` union, constructed type-directedly like - // process.envGet: the exit code wraps the f64 arm; signal - // death and spawn failure yield the interned null-arm - // instance (the runtime exposes a has_status flag + code). - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: spawnRes.status result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (f64Tag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: spawnRes.status union lacks its arms"); - } - const present = `scr_union_new_f64(${f64Tag}, scr_spawn_res_status(${arg(0)}))`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp( - e.type, - `scr_spawn_res_has_status(${arg(0)}) ? ${present} : ${absent}`, - ); - } - case "spawnRes.stdout": - return finish(`scr_spawn_res_stdout(${arg(0)})`); - case "spawnRes.stderr": - return finish(`scr_spawn_res_stderr(${arg(0)})`); - case "spawnRes.signal": { - // The `string | null` union (the termination signal's name, - // null for a normal exit or spawn failure) — the has/get pair - // wrapped type-directedly like spawnRes.status. - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: spawnRes.signal result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (strTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: spawnRes.signal union lacks its arms"); - } - const sv = emitter.newTemp( - STRING, - `scr_spawn_res_has_signal(${arg(0)}) ? scr_spawn_res_signal(${arg(0)}) : NULL`, - ); - emitter.moveTemp(sv); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${strTag}, ${sv.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${sv.name} ? ${present} : ${absent}`); - } - case "spawnRes.error": { - // The `Error | undefined` union, the envGet convention: a - // spawn failure hands back a fresh +1 %Error (ownership moves - // into the union box); otherwise the interned undefined arm. - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: spawnRes.error result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object" && a.className === "%Error") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (errTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: spawnRes.error union lacks its arms"); - } - const errT: IrType = { kind: "object", className: "%Error" }; - const ev = emitter.newTemp(errT, `scr_spawn_res_error(${arg(0)})`); - emitter.moveTemp(ev); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${errTag}, ${ev.name}, &scr_error_retain_v, &scr_error_release_v, ${emitter.traceArgC(errT)})`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${ev.name} ? ${present} : ${absent}`); - } - case "child.pid": - case "child.exitCode": { - // pid: `number | undefined` (undefined = spawn failure); - // exitCode: `number | null` (null while running / signal - // death) — the spawnRes.status construction over the has/get - // runtime pairs. - if (e.type.kind !== "union") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`); - } - const def = emitter.unionsById.get(e.type.unionId); - const wantUnit = e.fn === "child.pid" ? "undefinedT" : "nullT"; - const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; - const unitTag = def ? def.arms.findIndex((a) => a.kind === wantUnit) : -1; - if (f64Tag < 0 || unitTag < 0) { - throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`); - } - const has = e.fn === "child.pid" ? "scr_child_has_pid" : "scr_child_has_exit_code"; - const get = e.fn === "child.pid" ? "scr_child_pid" : "scr_child_exit_code"; - const present = `scr_union_new_f64(${f64Tag}, ${get}(${arg(0)}))`; - const absent = emitter.unitInstanceRef(e.type.unionId, unitTag); - return emitter.newTemp(e.type, `${has}(${arg(0)}) ? ${present} : ${absent}`); - } - case "child.stdin": - case "child.stdout": - case "child.stderr": { - // `Writable | null` / `Readable | null` — the child.pid - // pattern with a REF arm: the runtime answers a +1 stream - // handle or NULL (not piped). - if (e.type.kind !== "union") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`); - } - const def = emitter.unionsById.get(e.type.unionId); - const writer = e.fn === "child.stdin"; - const streamTag = def ? def.arms.findIndex((a) => a.kind === (writer ? "childWriter" : "childStream")) : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (streamTag < 0 || nullTag < 0) { - throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`); - } - const get = writer ? "scr_child_stdin" : e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr"; - const rawType = writer ? CHILDWRITER_T : CHILDSTREAM_T; - const raw = emitter.newTemp(rawType, `${get}(${arg(0)})`); - emitter.moveTemp(raw); // ownership passes into the union arm below - const stem = writer ? "scr_child_writer" : "scr_child_stream"; - const present = `scr_union_new_ref(${streamTag}, ${raw.name}, &${stem}_retain_v, &${stem}_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`); - } - case "writer.writeString": - return finish(`scr_child_writer_write_string(${arg(0)}, ${arg(1)})`); - case "writer.writeBytes": - return finish(`scr_child_writer_write_bytes(${arg(0)}, ${arg(1)})`); - case "writer.end": - return finish(`scr_child_writer_end(${arg(0)})`); - case "writer.destroy": - return finish(`scr_child_writer_destroy(${arg(0)})`); - case "writer.writable": - return finish(`scr_child_writer_writable(${arg(0)})`); - case "writer.onDrain": - case "writer.onFinish": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fnName = fn === "writer.onDrain" ? "scr_child_writer_on_drain" : "scr_child_writer_on_finish"; - emitter.line(`${fnName}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "writer.onError": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: writer.onError callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line(`scr_child_writer_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "procStream.write": - // The receiver IS the fd scalar; dispatches onto the exact - // promptly-submitted stdout/stderr paths (ordering identical). - return finish(`scr_proc_stream_write(${arg(0)}, ${arg(1)})`); - case "child.killed": - return finish(`scr_child_killed(${arg(0)})`); - case "child.kill": - // Throws the Unknown-signal TypeError on bad names (may-throw). - return finish(`scr_child_kill(${arg(0)}, ${arg(1)})`); - case "child.killNum": - return finish(`scr_child_kill_num(${arg(0)}, ${arg(1)})`); - case "child.unref": - return finish(`scr_child_unref(${arg(0)})`); - // child_process.spawn (scr_child.c + the event loop): the child - // starts NOW (posix_spawnp); the loop reaps it and fires its - // listeners. Never throws — spawn failure defers to "error". - case "cp.spawn": - emitter.usesTimers = true; // the loop must run to reap the child - return finish(`scr_spawn(${arg(0)}, ${arg(1)})`); - case "cp.fork": - emitter.usesTimers = true; - return finish( - `scr_fork(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)})`, - ); - case "cp.spawnOpts": - // The options form: per-slot stdio (ignore/inherit/fd — the - // fds dup2 into the child), detached (setsid), env - // replacement, cwd. Same loop/reap story as cp.spawn. - emitter.usesTimers = true; - return finish( - `scr_spawn_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, ${arg(9)}, ${arg(10)}, ${arg(11)})`, - ); - case "cp.execFile": { - const callbackArg = e.args[2]; - const cb = args[2]; - if (!callbackArg || !cb) throw new InternalCompilerError("emitter bug: execFile callback missing"); - const cbT = callbackArg.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: execFile callback not a func"); - emitter.moveTemp(cb); - const adapter = emitter.execFileThunkFor(cbT); - emitter.usesTimers = true; - return finish(`scr_exec_file(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter})`); - } - case "child.onExit": - case "child.onClose": { - // The callback MOVES into the child's registry; the third - // ingredient is the ADAPTER — emitted per callback shape, - // because the `number | null` union's tags are program data - // (a zero-param listener gets the runtime's ignoring thunk). - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 - ? "scr_child_exit_thunk0" - : cbT.params.length === 1 - ? emitter.childExitThunkFor(cbT.params[0]!) - : emitter.childExitSignalThunkFor(cbT.params[0]!, cbT.params[1]!); - emitter.line( - `${e.fn === "child.onClose" ? "scr_child_on_close" : "scr_child_on_exit"}(${arg(0)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`, - ); - return { name: "", type: e.type }; - } - case "child.onError": { - // Both error-listener shapes have runtime-provided adapters - // (constructing the %Error instance needs no program types). - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child.onError callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line( - `scr_child_on_error(${arg(0)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`, - ); - return { name: "", type: e.type }; - } - case "child.connected": - return finish(`scr_child_ipc_connected(${arg(0)})`); - case "child.send": - emitter.usesTimers = true; - return finish(`scr_child_ipc_send(${arg(0)}, ${arg(1)})`); - case "child.sendCb": { - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child.send callback not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - return finish(`scr_child_ipc_send_cb(${arg(0)}, ${arg(1)}, ${cb.name}, &${emitter.ipcSendThunkFor(cbT)})`); - } - case "child.disconnect": - emitter.usesTimers = true; - return finish(`scr_child_ipc_disconnect(${arg(0)})`); - case "child.onMessage": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child message listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - emitter.line(`scr_child_ipc_on_message(${arg(0)}, ${cb.name}, &${emitter.ipcMessageThunkFor(cbT)}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "child.onDisconnect": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: child disconnect listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - emitter.line(`scr_child_ipc_on_disconnect(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - default: - throw new InternalCompilerError(`emitter bug: childProcess libCall dispatch for ${fn}`); - } -} - -function emitNetworkLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:net (scr_net.c + the loop's net hook — linked only when - // these appear on the IR). Receivers and data are borrowed; - // CALLBACKS MOVE into the handle's registry (released at - // settlement). listen/connect make the loop live: usesTimers. - case "net.createServer": - return finish(`scr_net_create_server(NULL, NULL)`); - case "net.createServerCb": { - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: net.createServerCb handler not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; - return finish(`scr_net_create_server(${cb.name}, &${adapter})`); - } - case "net.listen": - emitter.usesTimers = true; // a listening server holds the loop open - emitter.line(`scr_net_listen(${arg(0)}, ${arg(1)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.listenCb": { - emitter.usesTimers = true; - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_listen(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.listenOpts": - emitter.usesTimers = true; - emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.listenOptsReusePort": - emitter.usesTimers = true; - emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.listenOptsCb": { - emitter.usesTimers = true; - // The callback slot may be the `(() => void) | undefined` - // optional-binding union: unwrap to a nullable closure (the - // createSecureServerSni pattern). - const cbT = e.args[4]!.type; - let cbExpr: string; - if (cbT.kind === "func") { - const cb = args[4]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - } else { - if (cbT.kind !== "union") throw new InternalCompilerError("emitter bug: net.listenOptsCb callback shape"); - const def = emitter.unionsById.get(cbT.unionId); - const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; - if (funcTag < 0) throw new InternalCompilerError("emitter bug: net.listenOptsCb union lacks its func arm"); - const u = args[4]!; - const t = emitter.newTemp( - def!.arms[funcTag]!, - `${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`, - ); - emitter.moveTemp(t); - cbExpr = t.name; - } - emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${cbExpr});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.listenOptsReusePortCb": { - emitter.usesTimers = true; - // The callback slot follows reusePort in the additive ABI and - // may be the `(() => void) | undefined` optional-binding union. - const cbT = e.args[5]!.type; - let cbExpr: string; - if (cbT.kind === "func") { - const cb = args[5]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - } else { - if (cbT.kind !== "union") throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb callback shape"); - const def = emitter.unionsById.get(cbT.unionId); - const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; - if (funcTag < 0) throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb union lacks its func arm"); - const u = args[5]!; - const t = emitter.newTemp( - def!.arms[funcTag]!, - `${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`, - ); - emitter.moveTemp(t); - cbExpr = t.name; - } - emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${cbExpr});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.serverPort": - return finish(`scr_net_server_port(${arg(0)})`); - case "net.serverListening": - return finish(`scr_net_server_listening(${arg(0)})`); - case "net.serverAddress": { - // The AddressInfo record from the three runtime reads (the - // dgram.address materialization; none of these throw). - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: net.serverAddress result is not a record"); - const ip = emitter.newTemp(STRING, `scr_net_server_addr_ip(${arg(0)})`); - const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - emitter.moveTemp(ip); - emitter.line(`${rec.name}->${mangleField("address")} = ${ip.name};`); - emitter.line(`${rec.name}->${mangleField("family")} = scr_net_server_addr_family(${arg(0)});`); - emitter.line(`${rec.name}->${mangleField("port")} = scr_net_server_port(${arg(0)});`); - return rec; - } - case "net.serverClose": - emitter.line(`scr_net_server_close(${arg(0)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.serverCloseCb": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_server_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.serverCloseBind": { - // The bound REAL close as a value: an emitted adapter behind - // a fresh closure whose one env slot holds the +1 server. - if (e.type.kind !== "func") throw new InternalCompilerError("emitter bug: net.serverCloseBind result not a func"); - const fnSym = emitter.closeBindThunkFor(e.type.params[0]!, e.type.ret.kind === "netServer"); - const bound = emitter.newTemp(e.type, `scr_closure_new((void *)&${fnSym}, 1)`); - emitter.line(`${bound.name}->caps[0] = scr_box_new_obj(&scr_net_server_retain_v, &scr_net_server_release_v, NULL);`); - emitter.line(`scr_box_set_ref(${bound.name}->caps[0], scr_net_server_retain(${arg(0)}));`); - return bound; - } - case "net.serverSetCloseOverride": { - // The override MOVES into the server's slot behind the - // emitted zero-arg wrapper (the runtime can't build the - // callback union — tags are program data). - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: close override not a func"); - const wrapSym = emitter.closeOverrideWrapFor(cbT.params[0]!, cbT.ret.kind === "netServer"); - const cb = args[1]!; - emitter.moveTemp(cb); // ownership moves into the wrapper's env box - const wrap = emitter.newTemp(e.args[1]!.type, `scr_closure_new((void *)&${wrapSym}, 1)`); - emitter.line(`${wrap.name}->caps[0] = scr_box_new(SCR_BOX_FUNC);`); - emitter.line(`scr_box_set_ref(${wrap.name}->caps[0], ${cb.name});`); - emitter.moveTemp(wrap); // and the wrapper moves into the server's slot - emitter.line(`scr_net_server_set_close_override(${arg(0)}, ${wrap.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.serverOnError": - case "net.sockOnError": { - // The child %Error adapters fit exactly (message → %Error). - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - const fn = e.fn === "net.serverOnError" ? "scr_net_server_on_error" : "scr_net_sock_on_error"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.serverOnClose": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_server_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.serverOnConnection": - case "net.serverOnSecureConnection": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; - const entry = e.fn === "net.serverOnConnection" ? "scr_net_server_on_connection" : "scr_net_server_on_secure_connection"; - emitter.line(`${entry}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.connect": - emitter.usesTimers = true; // a connecting/open socket holds the loop open - return finish(`scr_net_connect(${arg(0)}, ${arg(1)}, NULL)`); - case "net.connectAttempt": - // The validated autoSelectFamilyAttemptTimeout form: Node's - // range ladder runs first, the dial follows. - emitter.usesTimers = true; - return finish(`scr_net_connect_attempt(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "net.connectOptsChk": - // The runtime option-bag ladder (always throws — a validation - // error or the trailing fence; the error.nodeThrow dummy). - return finish( - `(scr_net_connect_opts_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "net.connectCb": { - emitter.usesTimers = true; - const cb = args[2]!; - emitter.moveTemp(cb); - return finish(`scr_net_connect(${arg(0)}, ${arg(1)}, ${cb.name})`); - } - case "net.connectLookup": { - // The caller-resolver dial: the runtime invokes the lookup - // closure with (hostname, dyn undefined, answer-closure); the - // answer closure's fn is the emitted per-shape thunk (its - // union tag and record field are program data — the SNI - // pattern). The lookup moves in. - emitter.usesTimers = true; - const lookupT = e.args[2]!.type; - if (lookupT.kind !== "func" || lookupT.params[2]?.kind !== "func") { - throw new InternalCompilerError("emitter bug: net.connectLookup resolver shape (frontend must fence)"); - } - const thunk = emitter.netLookupAnswerThunkFor(lookupT.params[2]); - const lookup = args[2]!; - emitter.moveTemp(lookup); - return finish(`scr_net_connect_lookup(${arg(0)}, ${arg(1)}, ${lookup.name}, (void *)&${thunk})`); - } - case "net.sockWrite": - emitter.line(`scr_net_sock_write_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockWriteBytes": - emitter.line(`scr_net_sock_write_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockEnd": - emitter.line(`scr_net_sock_end(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockEndStr": - emitter.line(`scr_net_sock_end_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockEndBytes": - emitter.line(`scr_net_sock_end_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockWriteDyn": - emitter.line(`scr_net_sock_write_dynv(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockEndDyn": - emitter.line(`scr_net_sock_end_dynv(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockDestroy": - emitter.line(`scr_net_sock_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockPause": - return finish(`scr_net_sock_pause(${arg(0)})`); - case "net.sockResume": - return finish(`scr_net_sock_resume(${arg(0)})`); - case "net.sockSetNoDelay": - return finish(`scr_net_sock_set_nodelay(${arg(0)}, ${arg(1)})`); - case "net.sockDestroySoon": - emitter.line(`scr_net_sock_destroy_soon(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockBytesWritten": - return finish(`scr_net_sock_bytes_written(${arg(0)})`); - case "net.sockReadable": - return finish(`scr_net_sock_readable(${arg(0)})`); - case "net.sockOnFinish": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_sock_on_finish(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockPipe": - emitter.line(`scr_net_sock_pipe(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockOnData": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: net.sockOnData callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_net_data_thunk0" - : cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" - : cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str" - : "scr_net_data_thunk_bytes"; - emitter.line(`scr_net_sock_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockOnEnd": - case "net.sockOnClose": - case "net.sockOnConnect": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fn = e.fn === "net.sockOnEnd" ? "scr_net_sock_on_end" - : e.fn === "net.sockOnClose" ? "scr_net_sock_on_close" - : "scr_net_sock_on_connect"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - // node:dgram + node:dns (scr_dgram.c + the loop's dgram hook — - // linked only when these appear on the IR). The net discipline: - // receivers/data borrowed, CALLBACKS MOVE into the handle's - // registry; bind/connect/send make the loop live (usesTimers). - case "dgram.createSocket": - return finish(`scr_dgram_create(${arg(0)})`); - case "dgram.bind": - emitter.usesTimers = true; // a bound socket holds the loop open - return finish(`scr_dgram_bind(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL)`); - case "dgram.bindCb": { - emitter.usesTimers = true; - const cb = args[3]!; - emitter.moveTemp(cb); - return finish(`scr_dgram_bind(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name})`); - } - case "dgram.connect": - emitter.usesTimers = true; // a connected socket holds the loop open - return finish(`scr_dgram_connect(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL)`); - case "dgram.connectCb": { - emitter.usesTimers = true; - const cb = args[3]!; - emitter.moveTemp(cb); - return finish(`scr_dgram_connect(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name})`); - } - case "dgram.sendStr": - emitter.usesTimers = true; // send implicit-binds; the socket stays open - return finish(`scr_dgram_send_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "dgram.sendBytes": - emitter.usesTimers = true; - return finish(`scr_dgram_send_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "dgram.sendChk": - emitter.usesTimers = true; // a validated send implicit-binds - return finish( - `scr_dgram_send_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`, - ); - case "dgram.address": { - // The AddressInfo record, built here from runtime parts (the - // frontend pinned the {address, family, port} shape). The - // address read is the fallible one — Node's "Not running" - // throw for a never-bound socket; family/port never throw - // once it passed. - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: dgram.address result is not a record"); - const ip = emitter.fallibleTemp(STRING, `scr_dgram_addr_ip(${arg(0)})`); - const rec = emitter.newTemp(e.type, `${mangleRecordNew(e.type.shapeId)}()`); - emitter.moveTemp(ip); - emitter.line(`${rec.name}->${mangleField("address")} = ${ip.name};`); - emitter.line(`${rec.name}->${mangleField("family")} = scr_dgram_addr_family(${arg(0)});`); - emitter.line(`${rec.name}->${mangleField("port")} = scr_dgram_addr_port(${arg(0)});`); - return rec; - } - case "dgram.close": - return finish(`scr_dgram_close(${arg(0)}, NULL)`); - case "dgram.closeCb": { - const cb = args[1]!; - emitter.moveTemp(cb); - return finish(`scr_dgram_close(${arg(0)}, ${cb.name})`); - } - case "dgram.unref": - emitter.line(`scr_dgram_unref(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "dgram.ref": - emitter.line(`scr_dgram_ref(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "dgram.onMessage": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: dgram.onMessage callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_dgram_msg_thunk0" - : cbT.params.length === 1 ? "scr_dgram_msg_thunk1" - : emitter.dgramMsgThunkFor(cbT.params[1]!); - emitter.line(`scr_dgram_on_message(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "dgram.onError": { - // The child %Error adapters fit exactly (message → %Error). - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: dgram.onError callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line(`scr_dgram_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "dgram.onListening": - case "dgram.onClose": - case "dgram.onConnect": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fn = e.fn === "dgram.onListening" ? "scr_dgram_on_listening" - : e.fn === "dgram.onClose" ? "scr_dgram_on_close" - : "scr_dgram_on_connect"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "dns.lookup": { - // getaddrinfo runs NOW; the callback (moved in) fires at the - // next loop turn through its per-shape adapter. - emitter.usesTimers = true; // the pending callback holds the loop open - const cbT = e.args[2]!.type; - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = emitter.dnsLookupThunkFor(cbT); - emitter.line(`scr_dns_lookup(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockEncrypted": { - // boolean | undefined: the true arm iff the socket carries a - // TLS transport; plain sockets answer the undefined arm (Node - // types `encrypted` on TLSSocket only). - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockEncrypted result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const boolTag = def ? def.arms.findIndex((a) => a.kind === "bool") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (boolTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: net.sockEncrypted union lacks its arms"); - } - const w = emitter.newTemp(BOOL, `scr_net_sock_encrypted(${arg(0)})`); - const present = `scr_union_new_bool(${boolTag}, true)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${w.name} ? ${present} : ${absent}`); - } - case "net.sockDestroyed": - return finish(`scr_net_sock_destroyed(${arg(0)})`); - case "net.sockWritable": - return finish(`scr_net_sock_writable(${arg(0)})`); - case "net.sockPipeRes": - emitter.line(`scr_http_sock_pipe_res(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.serverOnListening": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_server_on_listening(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockSetEncoding": - return finish(`scr_net_sock_set_encoding(${arg(0)}, ${arg(1)})`); - case "net.sockSetTimeout": - emitter.line(`scr_net_sock_set_timeout(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockOnTimeout": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_sock_on_timeout(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockOnReadable": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_net_sock_on_readable(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "net.sockRead": { - // Buffer | null, type-directed like http.reqHeader: NULL (not - // enough buffered) takes the null arm. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockRead result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (bytesTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: net.sockRead union lacks its arms"); - } - const b = emitter.newTemp(BYTES_U8, `scr_net_sock_read_bytes(${arg(0)}, ${arg(1)})`); - emitter.moveTemp(b); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${bytesTag}, ${b.name}, &scr_bytes_retain_v, &scr_bytes_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${b.name} ? ${present} : ${absent}`); - } - case "net.sockUnshift": - emitter.line(`scr_net_sock_unshift_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.serverEmitConnection": - emitter.line(`scr_net_server_emit_connection(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "net.sockRemoteAddress": { - // string | undefined, type-directed like http.reqHeader: NULL - // (closed socket) takes the undefined arm. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: net.sockRemoteAddress result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: net.sockRemoteAddress union lacks its arms"); - } - const s = emitter.newTemp(STRING, `scr_net_sock_remote_address(${arg(0)})`); - emitter.moveTemp(s); - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - case "net.getAutoSelTimeout": - return finish(`scr_net_get_autosel_timeout()`); - case "net.setAutoSelTimeout": - return finish(`scr_net_set_autosel_timeout(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: network libCall dispatch for ${fn}`); - } -} - -function emitTestLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:test (scr_test.c — linked only when these appear on the - // IR; moduleUsesNodeTest is the switch). Strings borrowed, - // callbacks MOVE. Registrations keep the loop-run emitted - // (usesTimers): the runner fiber drains after main returns. - case "test.register": { - emitter.usesTimers = true; - const cb = args[3]!; - emitter.moveTemp(cb); - emitter.line(`scr_test_register(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name}, ${arg(4)}, ${arg(5)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "test.registerEmpty": - emitter.usesTimers = true; - emitter.line(`scr_test_register(${arg(0)}, ${arg(1)}, ${arg(2)}, NULL, ${arg(3)}, ${arg(4)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "test.suite": { - emitter.usesTimers = true; - const cb = args[3]!; - emitter.moveTemp(cb); - emitter.line(`scr_test_suite(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cb.name}, ${arg(4)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "test.hook": { - emitter.usesTimers = true; - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_test_hook(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "test.sub": { - // Runs the subtest INLINE on the runner fiber; the settled - // promise (+1) is the await's operand. - const cb = args[4]!; - emitter.moveTemp(cb); - return finish(`scr_test_sub(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${cb.name}, ${arg(5)}, ${arg(6)})`); - } - case "test.subEmpty": - // Fn-less subtest: the settled promise is discarded here (the - // lowering types it void — nothing consumes it). - emitter.line(`scr_promise_release(scr_test_sub(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, NULL, 0, ${arg(4)}));${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "test.ctxSkip": - emitter.line(`scr_test_ctx_skip(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "test.ctxTodo": - emitter.line(`scr_test_ctx_todo(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "test.ctxDiagnostic": - emitter.line(`scr_test_ctx_diagnostic(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "test.ctxName": - return finish(`scr_test_ctx_name(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: test libCall dispatch for ${fn}`); - } -} - -function emitHttpLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:http, the server slice (scr_http.c over scr_net.c). - case "http.createServer": { - emitter.usesTimers = true; - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.createServer handler not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - return finish(`scr_http_create_server(${cb.name}, &${adapter})`); - } - case "http.reqUrl": - return finish(`scr_http_req_url(${arg(0)})`); - case "http.reqMethod": - return finish(`scr_http_req_method(${arg(0)})`); - case "http.reqHttpVersion": - return finish(`scr_http_req_http_version(${arg(0)})`); - case "http.reqHttpVersionMajor": - return finish(`scr_http_req_http_version_major(${arg(0)})`); - case "http.reqHttpVersionMinor": - return finish(`scr_http_req_http_version_minor(${arg(0)})`); - case "http.reqAborted": - return finish(`scr_http_req_aborted_flag(${arg(0)})`); - case "http.reqComplete": - return finish(`scr_http_req_complete(${arg(0)})`); - case "http.reqHeader": - case "http.reqTrailer": { - // string|undefined, type-directed exactly like process.envGet: - // the runtime answers +1 or NULL; NULL takes the undefined arm. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqHeader result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: http.reqHeader union lacks its arms"); - } - const entry = e.fn === "http.reqHeader" ? "scr_http_req_header" : "scr_http_req_trailer"; - const s = emitter.newTemp(STRING, `${entry}(${arg(0)}, ${arg(1)})`); - emitter.moveTemp(s); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - case "http.reqHeaderValues": - case "http.reqTrailerValues": { - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: distinct header result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const arrTag = def ? def.arms.findIndex((a) => a.kind === "array") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (arrTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: distinct header union lacks its arms"); - const entry = e.fn === "http.reqHeaderValues" ? "scr_http_req_header_values" : "scr_http_req_trailer_values"; - const values = emitter.newTemp(arrayOf(STRING), `${entry}(${arg(0)}, ${arg(1)})`); - emitter.moveTemp(values); - const present = `scr_union_new_ref(${arrTag}, ${values.name}, &scr_arr_retain_v, &scr_arr_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${values.name} ? ${present} : ${absent}`); - } - case "http.reqOnData": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.reqOnData callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_net_data_thunk0" - : cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" - : cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str" - : "scr_net_data_thunk_bytes"; - emitter.line(`scr_http_req_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.reqOnEnd": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_req_on_end(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.resSetHeader": - emitter.line(`scr_http_res_set_header(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.validateHeaderName": - emitter.line(`scr_http_validate_header_name(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.validateHeaderValue": - emitter.line(`scr_http_validate_header_value(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resWriteHead": - return finish(`scr_http_res_write_head(${arg(0)}, ${arg(1)})`); - case "http.resWriteHeadN": - return finish(`scr_http_res_write_head_n(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "http.resWriteContinue": - return finish(`scr_http_res_write_continue(${arg(0)})`); - case "http.resWriteProcessing": - return finish(`scr_http_res_write_processing(${arg(0)})`); - case "http.resWriteEarlyHints": - return finish(`scr_http_res_write_early_hints(${arg(0)}, ${arg(1)})`); - case "http.resWrite": - return finish(`scr_http_res_write_str(${arg(0)}, ${arg(1)})`); - case "http.resWriteBytes": - return finish(`scr_http_res_write_bytes(${arg(0)}, ${arg(1)})`); - case "http.resEnd": - return finish(`scr_http_res_end(${arg(0)})`); - case "http.resEndStr": - return finish(`scr_http_res_end_str(${arg(0)}, ${arg(1)})`); - case "http.resEndBytes": - return finish(`scr_http_res_end_bytes(${arg(0)}, ${arg(1)})`); - case "http.resWriteDyn": - return finish(`scr_http_res_write_dynv(${arg(0)}, ${arg(1)})`); - case "http.resEndDyn": - return finish(`scr_http_res_end_dynv(${arg(0)}, ${arg(1)})`); - case "http.resHeadersSent": - return finish(`scr_http_res_headers_sent(${arg(0)})`); - case "http.resWritableEnded": - return finish(`scr_http_res_writable_ended(${arg(0)})`); - case "http.resFlushHeaders": - return finish(`scr_http_res_flush_headers(${arg(0)})`); - case "http.resAddTrailers": - return finish(`scr_http_res_add_trailers(${arg(0)}, ${arg(1)})`); - case "http.resCork": - emitter.line(`scr_http_res_cork(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resUncork": - return finish(`scr_http_res_uncork(${arg(0)})`); - case "http.resWritableCorked": - return finish(`scr_http_res_writable_corked(${arg(0)})`); - // The server-surface member follow-ups. - case "http.reqStatusCode": { - // number | undefined, type-directed like process.columns: the - // runtime answers a negative status for server requests. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqStatusCode result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (f64Tag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: http.reqStatusCode union lacks its arms"); - } - const w = emitter.newTemp(F64, `scr_http_req_status(${arg(0)})`); - const present = `scr_union_new_f64(${f64Tag}, ${w.name})`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${w.name} >= 0 ? ${present} : ${absent}`); - } - case "http.reqSocket": - return finish(`scr_http_req_socket(${arg(0)})`); - case "http.reqH2Stream": { - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqH2Stream result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const streamTag = def ? def.arms.findIndex((a) => a.kind === "http2Stream") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (streamTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: http.reqH2Stream union lacks its arms"); - const st = emitter.newTemp({ kind: "http2Stream" }, `scr_http_req_h2_stream(${arg(0)})`); - emitter.moveTemp(st); - const present = `scr_union_new_ref(${streamTag}, ${st.name}, &scr_http2_stream_retain_v, &scr_http2_stream_release_v, NULL)`; - return emitter.newTemp(e.type, `${st.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`); - } - case "http.reqH2StreamOrThrow": - return finish(`scr_http_req_h2_stream_or_throw(${arg(0)}, ${arg(1)})`); - case "http.reqRawHeaders": - return finish(`scr_http_req_raw_headers(${arg(0)})`); - case "http.reqRawTrailers": - return finish(`scr_http_req_raw_trailers(${arg(0)})`); - case "http.reqHeaderPairs": - return finish(`scr_http_req_header_pairs(${arg(0)})`); - case "http.reqTrailerPairs": - return finish(`scr_http_req_trailer_pairs(${arg(0)})`); - case "http.reqStatusMessage": { - // string | undefined: a reason phrase on client responses, - // NULL (the undefined arm) on server requests — the - // sockRemoteAddress shape. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.reqStatusMessage result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: http.reqStatusMessage union lacks its arms"); - } - const m = emitter.newTemp(STRING, `scr_http_req_status_message(${arg(0)})`); - emitter.moveTemp(m); - const present = `scr_union_new_ref(${strTag}, ${m.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${m.name} ? ${present} : ${absent}`); - } - case "http.serverOnUpgrade": - case "http.clientOnUpgrade": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} listener not a func`); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 3 ? "scr_http_upgrade_thunk3" - : cbT.params.length === 2 ? "scr_http_upgrade_thunk2" - : cbT.params.length === 1 ? "scr_http_upgrade_thunk1" - : "scr_http_upgrade_thunk0"; - const entry = e.fn === "http.serverOnUpgrade" - ? "scr_http_server_on_upgrade" - : "scr_http_client_on_upgrade"; - emitter.line(`${entry}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.serverOnConnect": { - // The CONNECT handover: the upgrade adapters fit the plain - // socket-param shapes; a UNION socket slot (the h2 compat - // listener) takes the emitted per-shape wrapper — the arm's - // tag is program data. - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.serverOnConnect listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const p1 = cbT.params[1]; - const adapter = - p1 !== undefined && p1.kind === "union" - ? emitter.connectSockThunkFor(cbT) - : cbT.params.length === 3 ? "scr_http_upgrade_thunk3" - : cbT.params.length === 2 ? "scr_http_upgrade_thunk2" - : cbT.params.length === 1 ? "scr_http_upgrade_thunk1" - : "scr_http_upgrade_thunk0"; - const h2Def = p1?.kind === "union" ? emitter.unionsById.get(p1.unionId) : undefined; - const h2Adapter = cbT.params.length >= 2 - ? h2Def?.arms.some((arm) => arm.kind === "httpRes") ? `&${emitter.connectResThunkFor(cbT)}` : "NULL" - : cbT.params.length === 1 ? "&scr_http_handler_thunk1" : "&scr_http_handler_thunk0"; - emitter.line(`scr_http_server_on_connect(${arg(0)}, ${cb.name}, &${adapter}, ${h2Adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.reqPipeRes": - emitter.line(`scr_http_req_pipe_res(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.reqPipeClient": - emitter.line(`scr_http_req_pipe_client(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.reqPipeSock": - emitter.line(`scr_http_req_pipe_sock(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.reqResume": - emitter.line(`scr_http_req_resume(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.reqDestroy": - emitter.line(`scr_http_req_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.reqOnError": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.reqOnError callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line(`scr_http_req_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.reqOnClose": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_req_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.reqOnAborted": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_req_on_aborted(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.resDestroy": - emitter.line(`scr_http_res_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resOnClose": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_res_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.createServerEmpty": - emitter.usesTimers = true; - return finish(`scr_http_create_server(NULL, NULL)`); - case "http.serverJoinDupHeaders": - emitter.line(`scr_http_server_join_duplicate_headers(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.serverAllowMissingHostHeader": - emitter.line(`scr_http_server_allow_missing_host_header(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.serverSetTimeout": - emitter.line(`scr_net_server_set_timeout_plain(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.serverCloseAllConnections": - case "http.serverCloseIdleConnections": { - const entry = e.fn === "http.serverCloseAllConnections" - ? "scr_net_server_close_all_connections" : "scr_net_server_close_idle_connections"; - emitter.line(`${entry}(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.serverOnTimeout": - case "http.serverSetTimeoutCb": { - const cbIndex = e.fn === "http.serverSetTimeoutCb" ? 2 : 1; - const cbT = e.args[cbIndex]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`); - const cb = args[cbIndex]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; - const entry = e.fn === "http.serverOnTimeout" ? "scr_net_server_on_timeout" : "scr_net_server_set_timeout_cb"; - const callArgs = e.fn === "http.serverOnTimeout" - ? `${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)}` - : `${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter}`; - emitter.line(`${entry}(${callArgs});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.serverTimeoutGet": - return finish(`scr_net_server_timeout_get(${arg(0)}, ${arg(1)})`); - case "http.serverTimeoutSet": - emitter.line(`scr_net_server_timeout_set(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.serverTimeoutOptionSet": - return finish(`scr_net_server_timeout_option_set(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http.resStatusGet": - return finish(`scr_http_res_status_get(${arg(0)})`); - case "http.resStatusSet": - emitter.line(`scr_http_res_status_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resStatusMsgGet": { - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resStatusMsgGet result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) throw new InternalCompilerError("emitter bug: http.resStatusMsgGet union lacks its arms"); - const msg = emitter.newTemp(STRING, `scr_http_res_status_msg_get(${arg(0)})`); - emitter.moveTemp(msg); - const present = `scr_union_new_ref(${strTag}, ${msg.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - return emitter.newTemp(e.type, `${msg.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, undefTag)}`); - } - case "http.resStatusMsgSet": - emitter.line(`scr_http_res_status_msg_set(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resRequest": - return finish(`scr_http_res_request(${arg(0)})`); - case "http.resSocket": { - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: http.resSocket result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const socketTag = def ? def.arms.findIndex((a) => a.kind === "netSocket") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (socketTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: http.resSocket union lacks its arms"); - const sock = emitter.newTemp(NETSOCKET_T, `scr_http_res_socket(${arg(0)})`); - emitter.moveTemp(sock); - const present = `scr_union_new_ref(${socketTag}, ${sock.name}, &scr_net_sock_retain_v, &scr_net_sock_release_v, NULL)`; - return emitter.newTemp(e.type, `${sock.name} ? ${present} : ${emitter.unitInstanceRef(e.type.unionId, nullTag)}`); - } - case "http.resWritableFinished": - return finish(`scr_http_res_writable_finished(${arg(0)})`); - case "http.resSendDateGet": - return finish(`scr_http_res_send_date(${arg(0)})`); - case "http.resSendDateSet": - emitter.line(`scr_http_res_set_send_date(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resStrictContentLengthGet": - return finish(`scr_http_res_strict_content_length(${arg(0)})`); - case "http.resStrictContentLengthSet": - emitter.line(`scr_http_res_set_strict_content_length(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resSetTimeout": - emitter.line(`scr_http_res_set_timeout_plain(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resSetTimeoutCb": { - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_res_set_timeout(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.resGetHeader": - case "http.clientGetHeader": { - // string|undefined, exactly the http.reqHeader emission. - if (e.type.kind !== "union") throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: http.resGetHeader union lacks its arms"); - } - const entry = e.fn === "http.resGetHeader" ? "scr_http_res_get_header" : "scr_http_client_get_header"; - const s = emitter.newTemp(STRING, `${entry}(${arg(0)}, ${arg(1)})`); - emitter.moveTemp(s); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - case "http.resGetHeaderNames": - return finish(`scr_http_res_get_header_names(${arg(0)})`); - case "http.resGetRawHeaderNames": - return finish(`scr_http_res_get_raw_header_names(${arg(0)})`); - case "http.resGetHeaders": - return finish(`scr_http_res_get_headers(${arg(0)})`); - case "http.resHasHeader": - return finish(`scr_http_res_has_header_named(${arg(0)}, ${arg(1)})`); - case "http.resRemoveHeader": - emitter.line(`scr_http_res_remove_header(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.resOnFinish": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_res_on_finish(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.resWriteHeadPairs": - return finish(`scr_http_res_write_head_pairs(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http.resWriteHeadDyn": - return finish(`scr_http_res_write_head_dyn(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http.reqSetEncoding": - return finish(`scr_http_req_set_encoding(${arg(0)}, ${arg(1)})`); - case "http.reqSetTimeout": - return finish(`scr_http_req_set_timeout_plain(${arg(0)}, ${arg(1)})`); - case "http.reqSetTimeoutCb": { - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_req_set_timeout(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - // node:http, the client slice (scr_http.c over the net client). - case "http.request": - case "http.requestCb": { - emitter.usesTimers = true; // an in-flight request holds the loop open - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "http.requestCb") { - const cbT = e.args[7]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestCb callback not a func"); - const cb = args[7]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - return finish( - `scr_http_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${cbExpr}, ${adapter})`, - ); - } - case "http.agentNew": - emitter.usesTimers = true; // queued dials hold the loop open - return finish( - `scr_http_agent_new(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`, - ); - case "http.requestAgent": - case "http.requestAgentCb": { - emitter.usesTimers = true; - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "http.requestAgentCb") { - const cbT = e.args[8]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestAgentCb callback not a func"); - const cb = args[8]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - return finish( - `scr_http_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${cbExpr}, ${adapter})`, - ); - } - case "http.requestUrl": - case "http.requestUrlCb": - case "https.requestUrl": - case "https.requestUrlCb": { - emitter.usesTimers = true; // an in-flight request holds the loop open - const tls = e.fn.startsWith("https."); - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn.endsWith("Cb")) { - const cbT = e.args[3]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} callback not a func`); - const cb = args[3]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - const entry = tls ? "scr_https_request_url" : "scr_http_request_url"; - return finish( - `${entry}(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cbExpr}, ${adapter})`, - ); - } - case "http.requestConn": - case "http.requestConnCb": { - emitter.usesTimers = true; - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "http.requestConnCb") { - const cbT = e.args[6]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.requestConnCb callback not a func"); - const cb = args[6]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - const dial = args[0]!; - emitter.moveTemp(dial); - return finish( - `scr_http_request_conn(${dial.name}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${cbExpr}, ${adapter})`, - ); - } - case "https.createServer": { - emitter.usesTimers = true; - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.createServer handler not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - return finish( - `scr_https_create_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${cb.name}, &${adapter})`, - ); - } - case "https.createServerDyn": - emitter.usesTimers = true; - return finish(`scr_https_create_server_dyn(${arg(0)}, NULL, NULL)`); - case "https.createServerDynCb": { - emitter.usesTimers = true; - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.createServerDynCb handler not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - return finish(`scr_https_create_server_dyn(${arg(0)}, ${cb.name}, &${adapter})`); - } - case "http.serverOnRequest": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.serverOnRequest handler not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - emitter.line(`scr_http_server_on_request(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "https.request": - case "https.requestCb": { - emitter.usesTimers = true; // an in-flight request holds the loop open - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "https.requestCb") { - const cbT = e.args[9]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestCb callback not a func"); - const cb = args[9]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - return finish( - `scr_https_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${cbExpr}, ${adapter})`, - ); - } - case "https.requestAgent": - case "https.requestAgentCb": { - emitter.usesTimers = true; // an in-flight request holds the loop open - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "https.requestAgentCb") { - const cbT = e.args[10]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestAgentCb callback not a func"); - const cb = args[10]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - return finish( - `scr_https_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${arg(9)}, ${cbExpr}, ${adapter})`, - ); - } - case "https.requestFn": - case "https.requestFnCb": { - // The requestFn binding's runtime dial: arg 0 picks the client - // — a C ternary between the two real entry points over the - // SAME evaluated argument temps (only one side runs; the cb - // moves into whichever). The https row's reject/ca args are - // simply unused on the plain side, like Node's http.request - // with TLS options. - emitter.usesTimers = true; // an in-flight request holds the loop open - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "https.requestFnCb") { - const cbT = e.args[10]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: https.requestFnCb callback not a func"); - const cb = args[10]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res"; - } - return finish( - `(${arg(0)} ? scr_https_request(${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${arg(8)}, (const char *)${arg(9)}->data, ${arg(9)}->len, ${cbExpr}, ${adapter}) : scr_http_request_ex(${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${cbExpr}, ${adapter}, 80, NULL, NULL))`, - ); - } - case "http.clientWrite": - return finish(`scr_http_client_write_str(${arg(0)}, ${arg(1)})`); - case "http.clientSetHeader": - return finish(`scr_http_client_set_header(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http.clientHasHeader": - return finish(`scr_http_client_has_header_named(${arg(0)}, ${arg(1)})`); - case "http.clientRemoveHeader": - return finish(`scr_http_client_remove_header(${arg(0)}, ${arg(1)})`); - case "http.clientGetHeaderNames": - return finish(`scr_http_client_get_header_names(${arg(0)})`); - case "http.clientGetRawHeaderNames": - return finish(`scr_http_client_get_raw_header_names(${arg(0)})`); - case "http.clientGetHeaders": - return finish(`scr_http_client_get_headers(${arg(0)})`); - case "http.clientWriteBytes": - return finish(`scr_http_client_write_bytes(${arg(0)}, ${arg(1)})`); - case "http.clientEnd": - return finish(`scr_http_client_end(${arg(0)})`); - case "http.clientEndStr": - return finish(`scr_http_client_end_str(${arg(0)}, ${arg(1)})`); - case "http.clientEndBytes": - return finish(`scr_http_client_end_bytes(${arg(0)}, ${arg(1)})`); - case "http.clientWriteDyn": - return finish(`scr_http_client_write_dynv(${arg(0)}, ${arg(1)})`); - case "http.clientEndDyn": - return finish(`scr_http_client_end_dynv(${arg(0)}, ${arg(1)})`); - case "http.clientFlushHeaders": - return finish(`scr_http_client_flush_headers(${arg(0)})`); - case "http.clientAddTrailers": - return finish(`scr_http_client_add_trailers(${arg(0)}, ${arg(1)})`); - case "http.clientCork": - emitter.line(`scr_http_client_cork(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.clientUncork": - return finish(`scr_http_client_uncork(${arg(0)})`); - case "http.clientWritableCorked": - return finish(`scr_http_client_writable_corked(${arg(0)})`); - case "http.clientMethod": - return finish(`scr_http_client_method(${arg(0)})`); - case "http.clientPath": - return finish(`scr_http_client_path(${arg(0)})`); - case "http.clientHost": - return finish(`scr_http_client_host(${arg(0)})`); - case "http.clientProtocol": - return finish(`scr_http_client_protocol(${arg(0)})`); - case "http.clientHeadersSent": - return finish(`scr_http_client_headers_sent(${arg(0)})`); - case "http.clientWritableEnded": - return finish(`scr_http_client_writable_ended(${arg(0)})`); - case "http.clientWritableFinished": - return finish(`scr_http_client_writable_finished(${arg(0)})`); - case "http.clientSocket": - return finish(`scr_http_client_socket(${arg(0)})`); - case "http.clientReusedSocket": - return finish(`scr_http_client_reused_socket(${arg(0)})`); - case "http.clientSetNoDelay": - return finish(`scr_http_client_set_nodelay(${arg(0)}, ${arg(1)})`); - case "http.clientSetSocketKeepAlive": - return finish(`scr_http_client_set_socket_keepalive(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http.clientSetTimeout": - return finish(`scr_http_client_set_timeout(${arg(0)}, ${arg(1)})`); - case "http.clientSetTimeoutCb": { - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_http_client_set_timeout_cb(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.statusCodes": - return finish("scr_http_status_codes()"); - case "http.methods": - return finish("scr_http_methods()"); - case "http.clientDestroy": - emitter.line(`scr_http_client_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.clientAbort": - emitter.line(`scr_http_client_abort(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http.clientAborted": - return finish(`scr_http_client_aborted(${arg(0)})`); - case "http.clientDestroyed": - return finish(`scr_http_client_destroyed(${arg(0)})`); - case "http.clientOnResponse": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnResponse callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res"; - emitter.line(`scr_http_client_on_response(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.clientOnSocket": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnSocket callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; - emitter.line(`scr_http_client_on_socket(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.clientOnError": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http.clientOnError callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line(`scr_http_client_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http.clientOnTimeout": - case "http.clientOnClose": - case "http.clientOnFinish": - case "http.clientOnAbort": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fn = e.fn === "http.clientOnTimeout" ? "scr_http_client_on_timeout" - : e.fn === "http.clientOnClose" ? "scr_http_client_on_close" - : e.fn === "http.clientOnAbort" ? "scr_http_client_on_abort" : "scr_http_client_on_finish"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - default: - throw new InternalCompilerError(`emitter bug: http libCall dispatch for ${fn}`); - } -} - -function emitTlsLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:tls + node:https (scr_tls.c over scr_net.c/scr_http.c). - // cert/key/ca PEM arguments are strings OR Buffers — both carry - // data+len, so one emission shape serves both (the cast covers - // ScrBytes' uint8_t*). - case "tls.createServer": - case "tls.createServerCb": { - emitter.usesTimers = true; - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "tls.createServerCb") { - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.createServerCb handler not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_net_conn_thunk0" : "&scr_net_conn_thunk_sock"; - } - return finish( - `scr_tls_create_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${cbExpr}, ${adapter})`, - ); - } - // The RUNTIME options records (divergence 66's stance): the dyn - // walks throw the catchable fence for out-of-bounds members. - case "tls.pemDyn": - return finish(`scr_tls_pem_from_dyn(${arg(0)}, ${arg(1)}->data)`); - case "tls.createServerDyn": - case "tls.createServerDynCb": { - emitter.usesTimers = true; - let cbExpr = "NULL"; - let adapter = "NULL"; - if (e.fn === "tls.createServerDynCb") { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.createServerDynCb handler not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - adapter = cbT.params.length === 0 ? "&scr_net_conn_thunk0" : "&scr_net_conn_thunk_sock"; - } - return finish(`scr_tls_create_server_dyn(${arg(0)}, ${cbExpr}, ${adapter})`); - } - // The TLSSocket member surface on the socket kind. - case "tls.sockAuthorized": - return finish(`scr_tls_sock_authorized(${arg(0)})`); - case "tls.sockAuthError": { - // string | null: the verify-failure code string, or the null - // arm when authorized / never verified (Node's value shape). - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: tls.sockAuthError result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (strTag < 0 || nullTag < 0) { - throw new InternalCompilerError("emitter bug: tls.sockAuthError union lacks its arms"); - } - const s = emitter.newTemp(STRING, `scr_tls_sock_auth_error(${arg(0)})`); - emitter.moveTemp(s); - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, nullTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - case "tls.sockOnSecureConnect": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_tls_sock_on_secure_connect(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "tls.sockOnSession": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: tls.sockOnSession callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_net_data_thunk0" - : cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" - : "scr_net_data_thunk_bytes"; - emitter.line(`scr_tls_sock_on_session(${arg(0)}, ${cb.name}, (void *)&${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - // tls.connect (the TLS client socket): the callback moves in and - // fires post-handshake — the conn list's secureConnect timing. - case "tls.connect": - case "tls.connectCb": { - emitter.usesTimers = true; - let cbExpr = "NULL"; - if (e.fn === "tls.connectCb") { - const cb = args[3]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - } - return finish( - `scr_tls_connect_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${cbExpr})`, - ); - } - case "tls.createSecureContext": - // Parses the PEM pair into the opaque SecureContext handle the - // SNI answer serves; bad material is the construction-time - // print-and-die trap, like createServer's. - return finish( - `scr_tls_create_secure_context((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len)`, - ); - case "tls.createSecureContextDyn": - // The runtime option-bag form: Node's typed validations, then - // the pem walk (throws catchably on both ladders). - return finish(`scr_tls_create_secure_context_dyn(${arg(0)})`); - case "tls.caCertsChk": - // Always throws (validation error or the trailing fence). - return finish( - `(scr_tls_ca_certs_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - // The CA-store introspection unit (scr_tls_ca.c): cached - // per-type PEM string arrays (+1 retained answers), and the - // default-set replacement. get/set are may-throw seeds. - case "tlsca.get": - return finish(`scr_tls_ca_get(${arg(0)})`); - case "tlsca.root": - return finish(`scr_tls_ca_root()`); - case "tlsca.set": - return finish(`scr_tls_ca_set_default(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: tls libCall dispatch for ${fn}`); - } -} - -function emitHttp2LibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "http2.createSecureServer": - emitter.usesTimers = true; - return finish( - `scr_http2_create_secure_server_allow_http1((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, NULL, NULL, NULL, NULL, ${arg(2)})`, - ); - case "http2.createSecureServerH2": - // The REAL h2-over-TLS server (no allowHTTP1): ALPN advertises - // h2 alone and the h2 session attaches at establishment — - // 'stream'/'session' listeners, exactly the h2c surface. - emitter.usesTimers = true; - return finish( - `scr_http2_create_secure_server((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, ${arg(2)})`, - ); - case "http2.createSecureServerReq": - case "http2.createSecureServerH2Req": { - // createSecureServer(options, handler): the eager COMPAT - // handler is the first 'request' listener on either flavor - // (the createServerReq adapter family). - emitter.usesTimers = true; - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} handler not a func`); - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - const certKey = `(const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len`; - return finish( - fn === "http2.createSecureServerReq" - ? `scr_http2_create_secure_server_allow_http1(${certKey}, ${cb.name}, &${adapter}, NULL, NULL, ${arg(3)})` - : `scr_http2_create_secure_server_req(${certKey}, ${cb.name}, &${adapter}, ${arg(3)})`, - ); - } - case "http2.createSecureServerDyn": - // The runtime options record: allowHTTP1/cert/key read at - // runtime (may-throw — the divergence-66 walk). - emitter.usesTimers = true; - return finish(`scr_http2_create_secure_server_dyn(${arg(0)}, NULL, NULL)`); - case "http2.createSecureServerDynCb": { - emitter.usesTimers = true; - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: createSecureServerDynCb handler not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - return finish(`scr_http2_create_secure_server_dyn(${arg(0)}, ${cb.name}, &${adapter})`); - } - case "http2.createSecureServerSni": { - // The SNI form: the runtime parses each connection's - // ClientHello for server_name BEFORE the handshake, calls the - // JS SNICallback with (servername, answer-closure), and - // resumes when the answer arrives. The answer closure's fn is - // the emitted per-shape thunk (its unions' tags are program - // data). A union-typed arg is the conditional-spread spelling - // — the undefined arm passes NULL, exactly the no-SNI server. - emitter.usesTimers = true; - const sniT = e.args[2]!.type; - let cbFuncT: IrType; - let cbExpr: string; - if (sniT.kind === "func") { - cbFuncT = sniT; - const cb = args[2]!; - emitter.moveTemp(cb); - cbExpr = cb.name; - } else { - if (sniT.kind !== "union") throw new InternalCompilerError("emitter bug: createSecureServerSni callback shape"); - const def = emitter.unionsById.get(sniT.unionId); - const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1; - const funcArm = def?.arms[funcTag]; - if (funcTag < 0 || !funcArm) throw new InternalCompilerError("emitter bug: createSecureServerSni union lacks its func arm"); - cbFuncT = funcArm; - const u = args[2]!; - const t = emitter.newTemp( - funcArm, - `${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`, - ); - emitter.moveTemp(t); - cbExpr = t.name; - } - if (cbFuncT.kind !== "func" || cbFuncT.params[1]?.kind !== "func") { - throw new InternalCompilerError("emitter bug: createSecureServerSni callback shape (frontend must fence)"); - } - const answer = emitter.sniAnswerThunkFor(cbFuncT.params[1]); - return finish( - `scr_http2_create_secure_server_allow_http1((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len, NULL, NULL, ${cbExpr}, (void *)&${answer}, ${arg(3)})`, - ); - } - case "http2.serverOnSessionError": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionError listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 2 - ? "scr_http2_session_error_thunk2" - : cbT.params.length === 1 - ? "scr_http2_session_error_thunk1" - : "scr_http2_session_error_thunk0"; - emitter.line(`scr_http2_server_on_session_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.streamNoop": - // `req.stream?.on(...)`: stream is undefined on every - // connection the allowHTTP1 lowering accepts — the optional - // chain short-circuits and nothing runs. - return { name: "", type: e.type }; - case "http2.streamUndefCall": - // The unguarded form: Node's member read on undefined — the - // exact catchable TypeError (pending check via the may-throw - // seed set). Borrows the member-name string. - return finish(`scr_http2_stream_undef_call(${arg(0)})`); - // ── the REAL h2c surface (scr_http2.c) ───────────────────── - // Event registrations take the EMITTED adapter closure (built - // by the frontend — it owns arity and headers-record building), - // so the runtime thunk is a fixed passthrough per payload shape. - case "http2.createServer": - emitter.usesTimers = true; // a listening server holds the loop open - return finish(`scr_http2_create_server()`); - case "http2.createServerReq": { - // createServer(handler): the eager COMPAT handler — the first - // 'request' listener, the http.createServer adapter family. - emitter.usesTimers = true; - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http2.createServerReq handler not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 2 ? "scr_http_handler_thunk2" - : cbT.params.length === 1 ? "scr_http_handler_thunk1" - : "scr_http_handler_thunk0"; - return finish(`scr_http2_create_server_req(${cb.name}, &${adapter})`); - } - case "http2.serverOnStream": - case "http2.sessionOnStream": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fn = e.fn === "http2.serverOnStream" ? "scr_http2_server_on_stream" : "scr_http2_session_on_stream"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, &scr_http2_stream_thunk, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.serverOnSession": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: serverOnSession listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_http2_session_thunk0" : "scr_http2_session_thunk"; - emitter.line(`scr_http2_server_on_session(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionOnConnect": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionOnConnect listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length >= 2 ? "scr_http2_connect_thunk2" - : cbT.params.length === 1 ? "scr_http2_connect_thunk1" - : "scr_http2_connect_thunk0"; - emitter.line(`scr_http2_session_on_connect(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.connect": - emitter.usesTimers = true; // an open session holds the loop open - return finish( - `scr_http2_connect(${arg(0)}, ${arg(1)}, (const char *)${arg(2)}->data, ${arg(2)}->len, NULL, NULL)`, - ); - case "http2.connectCb": { - emitter.usesTimers = true; - const cbT = e.args[3]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: http2.connectCb listener not a func"); - const cb = args[3]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length >= 2 ? "scr_http2_connect_thunk2" - : cbT.params.length === 1 ? "scr_http2_connect_thunk1" - : "scr_http2_connect_thunk0"; - return finish( - `scr_http2_connect(${arg(0)}, ${arg(1)}, (const char *)${arg(2)}->data, ${arg(2)}->len, ${cb.name}, &${adapter})`, - ); - } - case "http2.sessionRequest": - return finish(`scr_http2_session_request(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "http2.sessionClose": - emitter.line(`scr_http2_session_close(${arg(0)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionCloseCb": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_session_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionDestroy": - emitter.line(`scr_http2_session_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionOnClose": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_session_on_close(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionOnError": - case "http2.streamOnError": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError(`emitter bug: ${e.fn} listener not a func`); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - const fn = e.fn === "http2.sessionOnError" ? "scr_http2_session_on_error" : "scr_http2_stream_on_error"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionOnGoaway": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: sessionOnGoaway listener not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length >= 2 ? "scr_http2_goaway_thunk2" - : cbT.params.length === 1 ? "scr_http2_goaway_thunk1" - : "scr_http2_goaway_thunk0"; - emitter.line(`scr_http2_session_on_goaway(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionClosed": - return finish(`scr_http2_session_closed(${arg(0)})`); - case "http2.sessionDestroyed": - return finish(`scr_http2_session_destroyed(${arg(0)})`); - case "http2.sessionEncrypted": - return finish(`scr_http2_session_encrypted(${arg(0)})`); - case "http2.sessionType": - return finish(`scr_http2_session_type(${arg(0)})`); - case "http2.sessionAlpn": - return finish(`scr_http2_session_alpn(${arg(0)})`); - case "http2.sessionSocket": - return finish(`scr_http2_session_socket(${arg(0)})`); - case "http2.streamRespond": - emitter.line(`scr_http2_stream_respond(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamWrite": - emitter.line(`scr_http2_stream_write_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamWriteBytes": - emitter.line(`scr_http2_stream_write_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamEnd": - emitter.line(`scr_http2_stream_end(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamEndStr": - emitter.line(`scr_http2_stream_end_str(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamEndBytes": - emitter.line(`scr_http2_stream_end_bytes(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamClose": - emitter.line(`scr_http2_stream_close(${arg(0)}, ${arg(1)}, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamCloseCb": { - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_stream_close(${arg(0)}, ${arg(1)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.streamDestroy": - emitter.line(`scr_http2_stream_destroy(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionSettings0": - emitter.line(`scr_http2_session_settings(${arg(0)}, NULL, NULL, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionSettings": - emitter.line(`scr_http2_session_settings(${arg(0)}, ${arg(1)}, NULL, NULL);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionSettingsDynCb": - emitter.line(`scr_http2_session_settings_dyncb(${arg(0)}, ${arg(1)}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionSettingsCb0": { - const cb = args[2]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_session_settings(${arg(0)}, ${arg(1)}, ${cb.name}, &scr_http2_settings_thunk0);${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionOnSettingsDyn": - emitter.line(`scr_http2_session_on_settings_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.sessionOnSettings0": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_session_on_settings(${arg(0)}, ${cb.name}, &scr_http2_settings_thunk0, ${arg(2)}, ${arg(3)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.sessionSettingsGet": - return finish(`scr_http2_session_settings_get(${arg(0)}, ${arg(1)})`); - case "http2.sessionPendingSettingsAck": - return finish(`scr_http2_session_pending_settings_ack(${arg(0)})`); - case "http2.getDefaultSettings": - return finish(`scr_http2_get_default_settings()`); - case "http2.streamSetEncoding": - emitter.line(`scr_http2_stream_set_encoding(${arg(0)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamSetEncodingRet": - // the chaining spelling: same write, answers the receiver +1 - return finish(`scr_http2_stream_set_encoding_ret(${arg(0)}, ${arg(1)})`); - case "http2.streamResume": - emitter.line(`scr_http2_stream_resume(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamPause": - emitter.line(`scr_http2_stream_pause(${arg(0)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - case "http2.streamOnData": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: streamOnData callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_net_data_thunk0" - : cbT.params[0]!.kind === "dyn" ? "scr_net_data_thunk_dyn" - : cbT.params[0]!.kind === "string" ? "scr_net_data_thunk_str" - : "scr_net_data_thunk_bytes"; - emitter.line(`scr_http2_stream_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.streamOnEnd": - case "http2.streamOnClose": - case "http2.streamOnAborted": { - const cb = args[1]!; - emitter.moveTemp(cb); - const fn = e.fn === "http2.streamOnEnd" ? "scr_http2_stream_on_end" - : e.fn === "http2.streamOnClose" ? "scr_http2_stream_on_close" - : "scr_http2_stream_on_aborted"; - emitter.line(`${fn}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.streamOnResponse": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_http2_stream_on_response(${arg(0)}, ${cb.name}, &scr_http2_resp_thunk, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "http2.streamId": - return finish(`scr_http2_stream_id(${arg(0)})`); - case "http2.streamRstCode": - return finish(`scr_http2_stream_rst_code(${arg(0)})`); - case "http2.streamDestroyed": - return finish(`scr_http2_stream_destroyed(${arg(0)})`); - case "http2.streamClosed": - return finish(`scr_http2_stream_closed(${arg(0)})`); - case "http2.streamAborted": - return finish(`scr_http2_stream_aborted(${arg(0)})`); - case "http2.streamPending": - return finish(`scr_http2_stream_pending(${arg(0)})`); - case "http2.streamSession": - return finish(`scr_http2_stream_session(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: http2 libCall dispatch for ${fn}`); - } -} - -function emitAsyncContextLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "tp.setTimeout": - return finish(`scr_tp_set_timeout(${arg(0)})`); - case "tp.setImmediate": - return finish(`scr_tp_set_immediate()`); - case "dc.channel": - return finish(`scr_dc_channel(${arg(0)})`); - case "dc.subscribe": - return finish(`scr_dc_subscribe(${arg(0)}, ${arg(1)})`); - case "dc.unsubscribe": - return finish(`scr_dc_unsubscribe(${arg(0)}, ${arg(1)})`); - case "dc.hasSubscribers": - return finish(`scr_dc_has_subscribers(${arg(0)})`); - case "dc.publish": - return finish(`scr_dc_publish(${arg(0)}, ${arg(1)})`); - case "dc.chanSubscribe": - return finish(`scr_dc_chan_subscribe(${arg(0)}, ${arg(1)})`); - case "dc.chanUnsubscribe": - return finish(`scr_dc_chan_unsubscribe(${arg(0)}, ${arg(1)})`); - case "dc.chanHasSubscribers": - return finish(`scr_dc_chan_has_subscribers(${arg(0)})`); - case "dc.chanName": - return finish(`scr_dc_chan_name(${arg(0)})`); - case "timers.setImmediateFnValue": - // Calling the minted value schedules a real immediate — the - // loop must run to fire it. - emitter.usesTimers = true; - return finish(`scr_set_immediate_dyn_value()`); - case "timers.immediatePromise": - emitter.usesTimers = true; - return finish(`scr_immediate_promise()`); - case "dc.tracingChannel": - return finish(`scr_dc_tracing_channel(${arg(0)})`); - case "dc.tracingChannelOf": - return finish(`scr_dc_tracing_channel_of(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "dc.tcChannel": - return finish(`scr_dc_tc_channel(${arg(0)}, ${arg(1)})`); - case "dc.tcHasSubscribers": - return finish(`scr_dc_tc_has_subscribers(${arg(0)})`); - case "dc.tcSubscribe": - return finish(`scr_dc_tc_subscribe(${arg(0)}, ${arg(1)})`); - case "dc.tcUnsubscribe": - return finish(`scr_dc_tc_unsubscribe(${arg(0)}, ${arg(1)})`); - case "dc.tcTraceSync": - return finish(`scr_dc_tc_trace_sync(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "dc.tcTraceCallback": - return finish(`scr_dc_tc_trace_callback(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "async.hop": - emitter.usesTimers = true; - return finish(`scr_await_hop()`); - case "async.awaitDyn": - emitter.usesTimers = true; // the hop rides the loop - return finish(`scr_await_dyn_value(${arg(0)})`); - case "als.new": - return finish(`scr_als_new()`); - case "als.get": - return finish(`scr_als_get(${arg(0)})`); - case "als.run": - return finish(`scr_als_run(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "als.exitRun": - return finish(`scr_als_exit_run(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "als.enterWith": - return finish(`scr_als_enter_with(${arg(0)}, ${arg(1)})`); - case "als.disable": - return finish(`scr_als_disable(${arg(0)})`); - case "dc.chanBindStore": - return finish(`scr_dc_chan_bind_store(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "dc.chanUnbindStore": - return finish(`scr_dc_chan_unbind_store(${arg(0)}, ${arg(1)})`); - case "dc.chanRunStores": - return finish(`scr_dc_chan_run_stores(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "dc.tcTracePromise": - // The reaction fiber needs the loop to drain it. - emitter.usesTimers = true; - return finish(`scr_dc_tc_trace_promise(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "timers.setTimeout": { - // The loop owns the callback until it fires. - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_set_timeout(${cb.name}, ${arg(1)})`); - } - case "timers.setInterval": { - // The loop owns the callback until clearInterval; the f64 - // handle comes back. A live interval keeps the loop alive. - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_set_interval(${cb.name}, ${arg(1)})`); - } - case "timers.clearInterval": - return finish(`scr_clear_interval(${arg(0)})`); - case "timers.setTimeoutHandle": { - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_set_timeout_handle(${cb.name}, ${arg(1)})`); - } - case "timers.clearTimeout": - return finish(`scr_clear_interval(${arg(0)})`); - case "timers.unref": - // Returns the handle for chaining (comma expr: bookkeep, yield). - return emitter.newTemp(e.type, `(scr_timer_unref(${arg(0)}), ${arg(0)})`); - case "timers.ref": - return emitter.newTemp(e.type, `(scr_timer_ref(${arg(0)}), ${arg(0)})`); - case "timers.hasRef": - return finish(`scr_timer_has_ref(${arg(0)})`); - case "timers.refresh": - return emitter.newTemp(e.type, `(scr_timer_refresh(${arg(0)}), ${arg(0)})`); - case "timers.setImmediate": { - // The loop owns the callback until the check phase fires it. - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_set_immediate(${cb.name})`); - } - case "timers.queueMicrotask": { - // The envelope owns the callback until the drain runs it; the - // loop must run so a main-queued microtask fires after %main. - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_queue_microtask(${cb.name})`); - } - case "timers.queueMicrotaskDyn": - // Borrowed dyn; a non-function throws ERR_INVALID_ARG_TYPE - // synchronously (may-throw seed set). - emitter.usesTimers = true; - return finish(`scr_queue_microtask_dyn(${arg(0)})`); - case "timers.clearImmediate": - return finish(`scr_clear_immediate(${arg(0)})`); - case "timers.immediateUnref": - return emitter.newTemp(e.type, `(scr_immediate_unref(${arg(0)}), ${arg(0)})`); - case "timers.immediateRef": - return emitter.newTemp(e.type, `(scr_immediate_ref(${arg(0)}), ${arg(0)})`); - case "timers.immediateHasRef": - return finish(`scr_immediate_has_ref(${arg(0)})`); - case "timers.clearNoop": - // clearTimeout(null) and friends: Node silently ignores - // non-handles — nothing runs. - return { name: "", type: e.type }; - default: - throw new InternalCompilerError(`emitter bug: asyncContext libCall dispatch for ${fn}`); - } -} - -function emitProcessLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "process.stdoutWriteBytes": - return finish(`scr_process_stdout_write_bytes(${arg(0)}, ${arg(1)})`); - case "process.stderrWriteBytes": - return finish(`scr_process_stderr_write_bytes(${arg(0)}, ${arg(1)})`); - case "process.stdoutWriteBytesCb": - case "process.stderrWriteBytesCb": { - // Submit the bytes only after every call argument evaluated, - // then move the completion callback onto the next-tick queue. - // The shared error-first adapter materializes success `null`. - emitter.usesTimers = true; - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process write callback not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = emitter.fsRenameThunkFor(cbT); - const write = e.fn === "process.stdoutWriteBytesCb" - ? "scr_process_stdout_write_bytes" - : "scr_process_stderr_write_bytes"; - const out = emitter.newTemp(e.type, `${write}(${arg(0)}, ${arg(1)})`); - emitter.line(`scr_process_write_callback(${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`); - return out; - } - case "process.onWarning": - return finish(`scr_process_on_warning(${arg(0)})`); - case "process.offWarning": - return finish(`scr_process_off_warning(${arg(0)})`); - case "process.emitWarning": - return finish(`scr_process_emit_warning(${arg(0)})`); - case "process.onUncaughtException": - emitter.usesTimers = true; - return finish(`scr_process_on_uncaught_exception(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "process.offUncaughtException": - return finish(`scr_process_off_uncaught_exception(${arg(0)}, ${arg(1)})`); - case "process.onUnhandledRejection": - // The completed-checkpoint report dispatches the listeners. - emitter.usesTimers = true; - return finish(`scr_process_on_unhandled_rejection(${arg(0)}, ${arg(1)})`); - case "process.offUnhandledRejection": - return finish(`scr_process_off_unhandled_rejection(${arg(0)})`); - case "process.onRejectionHandled": - // Fires after a reported promise gains a handler — the loop - // must be live for the checkpoint report to run. - emitter.usesTimers = true; - return finish(`scr_process_on_rejection_handled(${arg(0)}, ${arg(1)})`); - case "process.offRejectionHandled": - return finish(`scr_process_off_rejection_handled(${arg(0)})`); - case "process.argv": - return finish(`scr_process_argv()`); - case "process.platform": - return finish(`scr_process_platform()`); - case "process.cwd": - return finish(`scr_process_cwd()`); - case "process.stdoutWrite": - return finish(`scr_process_stdout_write(${arg(0)})`); - case "process.stdio": - emitter.usesTimers = true; - return finish(`scr_process_stdio(${arg(0)})`); - case "process.stderrWrite": - return finish(`scr_process_stderr_write(${arg(0)})`); - case "process.envGet": { - // getenv(3): +1 fresh string, or NULL when unset. The union - // construction is type-directed HERE — the runtime knows no - // tags: present wraps the string arm (ownership of the fresh - // string MOVES into the box), absent yields the interned - // immortal undefined-arm instance (releases no-op on it). - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: process.envGet result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: process.envGet union lacks its arms"); - } - const s = emitter.newTemp(STRING, `scr_env_get(${arg(0)})`); - emitter.moveTemp(s); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - case "process.envSet": - return finish(`scr_env_set(${arg(0)}, ${arg(1)})`); - case "process.envUnset": - return finish(`scr_env_unset(${arg(0)})`); - case "process.envPairs": - return finish(`scr_env_pairs()`); - case "process.pid": - return finish(`scr_process_pid()`); - case "process.getuid": - return finish(`scr_process_getuid()`); - case "process.uptime": - return finish(`scr_process_uptime()`); - case "module.registryInit": - return finish(`scr_module_registry_init(${arg(0)})`); - case "module.define": - return finish(`scr_module_define(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "module.enter": - return finish(`scr_module_enter(${arg(0)})`); - case "module.link": - return finish(`scr_module_link(${arg(0)}, ${arg(1)})`); - case "module.finish": - return finish(`scr_module_finish(${arg(0)})`); - case "module.fail": - return finish(`scr_module_fail(${arg(0)})`); - case "module.filename": - return finish(`scr_module_filename(${arg(0)})`); - case "module.id": - return finish(`scr_module_id(${arg(0)})`); - case "module.path": - return finish(`scr_module_path(${arg(0)})`); - case "module.paths": - return finish(`scr_module_paths(${arg(0)})`); - case "module.children": - return finish(`scr_module_children(${arg(0)})`); - case "module.parent": - return finish(`scr_module_parent(${arg(0)})`); - case "module.loaded": - return finish(`scr_module_loaded(${arg(0)})`); - case "module.cacheGet": - return finish(`scr_module_cache_get(${arg(0)})`); - case "module.cacheHas": - return finish(`scr_module_cache_has(${arg(0)})`); - case "module.cacheKeys": - return finish(`scr_module_cache_keys()`); - case "process.availableMemory": - return finish(`scr_available_memory()`); - case "process.constrainedMemory": - return finish(`scr_constrained_memory()`); - case "process.cpuUser": - return finish(`scr_cpu_user()`); - case "process.cpuSystem": - return finish(`scr_cpu_system()`); - case "process.cpuUserDiff": - return finish(`scr_cpu_user_diff(${arg(0)})`); - case "process.cpuSystemDiff": - return finish(`scr_cpu_system_diff(${arg(0)})`); - case "process.threadCpuUser": - return finish(`scr_thread_cpu_user()`); - case "process.threadCpuSystem": - return finish(`scr_thread_cpu_system()`); - case "process.threadCpuUserDiff": - return finish(`scr_thread_cpu_user_diff(${arg(0)})`); - case "process.threadCpuSystemDiff": - return finish(`scr_thread_cpu_system_diff(${arg(0)})`); - case "process.cpuPrevValidate": - // MAY THROW: negative/non-finite prev fields raise Node's - // ERR_INVALID_ARG_VALUE RangeError (finish emits the check). - return finish(`scr_cpu_prev_validate(${arg(0)}, ${arg(1)})`); - case "process.rusage": - return finish(`scr_process_rusage(${arg(0)})`); - case "process.activeResources": - // The loop's bookkeeping needs the loop linked. - emitter.usesTimers = true; - return finish(`scr_active_resources()`); - case "process.getgid": - return finish(`scr_process_getgid()`); - case "process.execPath": - return finish(`scr_process_exec_path()`); - case "process.forkTarget": - return finish(`scr_process_fork_target(${arg(0)})`); - case "process.connected": - return finish(`scr_process_ipc_connected()`); - case "process.send": - emitter.usesTimers = true; - return finish(`scr_process_ipc_send(${arg(0)})`); - case "process.sendCb": { - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process.send callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - return finish(`scr_process_ipc_send_cb(${arg(0)}, ${cb.name}, &${emitter.ipcSendThunkFor(cbT)})`); - } - case "process.disconnect": - emitter.usesTimers = true; - return finish(`scr_process_ipc_disconnect()`); - case "process.onMessage": { - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process message listener not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - emitter.line(`scr_process_ipc_on_message(${cb.name}, &${emitter.ipcMessageThunkFor(cbT)}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "process.onDisconnect": { - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process disconnect listener not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - emitter.usesTimers = true; - emitter.line(`scr_process_ipc_on_disconnect(${cb.name}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "process.arch": - return finish(`scr_process_arch()`); - case "process.versions": - return finish(`scr_process_versions()`); - case "process.builtinId": - return finish(`scr_process_builtin_id(${arg(0)}, ${arg(1)})`); - case "process.builtinModule": - return finish(`scr_process_builtin_module(${arg(0)}, ${arg(1)})`); - case "process.builtinUnsupported": - return finish(`scr_process_builtin_unsupported(${arg(0)}, ${arg(1)})`); - case "process.versionsNode": - return finish(`scr_process_versions_node()`); - case "process.versionsOpenssl": - return finish(`scr_process_versions_openssl()`); - case "process.umask": - return finish(`scr_process_umask(${arg(0)})`); - case "process.chdir": - return finish(`scr_process_chdir(${arg(0)})`); - case "process.exiting": - return finish(`scr_process_exiting()`); - case "process.kill": - // Throws Node's kill errors (bad pid TypeError, unknown-signal - // TypeError, ESRCH/EPERM Error); the pending check comes from - // the may-throw set. - return finish(`scr_process_kill_named(${arg(0)}, ${arg(1)})`); - case "process.killNum": - return finish(`scr_process_kill(${arg(0)}, ${arg(1)})`); - case "process.exit": - // Flushes stdout and _Exit()s — never returns (exit handlers, - // including the RC audit, deliberately do not run). - return finish(`scr_process_exit(${arg(0)})`); - case "process.setExitCode": - return finish(`scr_process_set_exit_code(${arg(0)})`); - case "process.currentExitCode": - return finish(`scr_process_exit_code_or_zero()`); - case "process.nextTick": { - // The tick queue owns the callback until the drain fires it; - // ticks run before promise jobs at every loop checkpoint and - // keep the loop alive while queued, so main runs the loop. - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_next_tick(${cb.name})`); - } - case "process.onSignal": - emitter.usesTimers = true; - return finish(`scr_signal_on(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "process.offSignal": - return finish(`scr_signal_off(${arg(0)}, ${arg(1)})`); - case "process.onExit": { - // Runtime adapters cover both shapes (the code is a plain - // double); the registry owns the callback. - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: process.onExit callback not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code"; - emitter.line(`scr_process_on_exit(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "process.offExit": - return finish(`scr_process_off_exit(${arg(0)})`); - case "stdin.onData": { - // A data listener is a consumer: the loop watches fd 0 and - // stays alive until EOF, so main must run it. - emitter.usesTimers = true; - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stdin.onData callback not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_stdin_data_thunk0" : "scr_stdin_data_thunk_bytes"; - emitter.line(`scr_stdin_on_data(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "stdin.onEnd": { - emitter.usesTimers = true; - const cb = args[0]!; - emitter.moveTemp(cb); - return finish(`scr_stdin_on_end(${cb.name}, ${arg(1)})`); - } - case "stdin.onError": { - // The child error adapters fit exactly (message → %Error). - emitter.usesTimers = true; - const cbT = e.args[0]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stdin.onError callback not a func"); - const cb = args[0]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - emitter.line(`scr_stdin_on_error(${cb.name}, &${adapter}, ${arg(1)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "stdin.nextChunk": - // +1 promise of the next chunk (empty = EOF); the await parks - // the fiber while the loop watches fd 0. - emitter.usesTimers = true; - return finish(`scr_stdin_next_chunk()`); - case "process.isTTY": - return finish(`scr_process_is_tty(${arg(0)})`); - case "process.columns": - case "process.rows": { - // ioctl(TIOCGWINSZ): a non-negative dimension wraps the f64 arm; - // a non-TTY stream (or an ioctl refusal) comes back negative - // and yields the interned undefined-arm instance — Node's - // missing geometry. Type-directed union construction, like - // process.envGet. - if (e.type.kind !== "union") { - throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`); - } - const def = emitter.unionsById.get(e.type.unionId); - const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (f64Tag < 0 || undefTag < 0) { - throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`); - } - const w = emitter.newTemp(F64, `scr_process_${e.fn === "process.rows" ? "rows" : "columns"}(${arg(0)})`); - const present = `scr_union_new_f64(${f64Tag}, ${w.name})`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${w.name} >= 0 ? ${present} : ${absent}`); - } - case "process.stdinDestroy": - // A deliberate no-op (SEMANTICS.md). - return finish(`scr_process_stdin_destroy()`); - case "process.stdinSetRawMode": - // TTY: termios raw mode (libuv's UV_TTY_MODE_RAW set). Non-TTY: - // Node's exact catchable TypeError (the may-throw seed). - return finish(`scr_process_stdin_set_raw_mode(${arg(0)})`); - default: - throw new InternalCompilerError(`emitter bug: process libCall dispatch for ${fn}`); - } -} - -function emitErrorsEventsLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "error.nodeThrow": - // The compiler-resolved Node-parity throw (always throws — - // the typed dummy is abandoned by the pending check's - // unwind; releases are NULL-tolerant). Borrows both strings. - return finish( - `(scr_throw_node_coded(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "error.argTypeThrow": - // Always throws with the runtime-rendered Received tail (the - // error.nodeThrow dummy pattern). Borrows all three. - return finish( - `(scr_throw_arg_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "error.propTypeThrow": - // The property flavor ("The \"options.x\" property must be - // ...") — same always-throw dummy pattern. - return finish( - `(scr_throw_prop_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, - ); - case "error.newOptions": - case "error.new": { - // Which builtin the runtime constructs is named by the RESULT - // type. Arguments are borrowed; raw message conversion may throw. - if (e.type.kind !== "object") throw new InternalCompilerError("emitter bug: error.new result is not a class"); - const rec = RUNTIME_ERROR_CLASSES.get(e.type.className); - if (!rec) throw new InternalCompilerError(`emitter bug: error.new of ${e.type.className}`); - return finish(fn === "error.newOptions" - ? `scr_error_new_options(${rec.kind}, ${arg(0)}, ${arg(1)})` - : `scr_error_new(${rec.kind}, ${arg(0)})`); - } - case "error.ctorOptions": - case "error.ctor": { - // super(message) into the builtin base: stamps name/message on - // the receiver (borrowed, like the message). The RECEIVER'S - // static class names which builtin name to stamp. - const recvT = e.args[0]!.type; - if (recvT.kind !== "object") throw new InternalCompilerError("emitter bug: error.ctor receiver is not a class"); - const rec = RUNTIME_ERROR_CLASSES.get(recvT.className); - if (!rec) throw new InternalCompilerError(`emitter bug: error.ctor on ${recvT.className}`); - return finish(fn === "error.ctorOptions" - ? `scr_error_init_options(${arg(0)}, ${rec.kind}, ${arg(1)}, ${arg(2)})` - : `scr_error_init(${arg(0)}, ${rec.kind}, ${arg(1)})`); - } - case "error.cause": - return finish(`scr_error_cause((ScrError *)${arg(0)})`); - case "error.hasCause": - return finish(`scr_error_has_cause((ScrError *)${arg(0)})`); - case "error.setCause": - return finish(`scr_error_set_cause((ScrError *)${arg(0)}, ${arg(1)})`); - case "error.deleteCause": - return finish(`scr_error_delete_cause((ScrError *)${arg(0)})`); - case "error.toString": - // Borrowed receiver; +1 "name: message" (Node's toString rules). - return finish(`scr_error_to_string((ScrError *)${arg(0)})`); - case "error.newDom": - // new DOMException(message?, nameOrOptions?) — both dyn args - // borrowed (WebIDL resolution runs in the runtime); +1 - // %DOMException. Never throws. - return finish(`scr_domex_new(${arg(0)}, ${arg(1)})`); - case "error.domCode": - // The WebIDL legacy numeric code (0 off-table). Borrowed. - return finish(`scr_domex_code(${arg(0)})`); - case "error.domHasCause": - // `'cause' in e` — the options form's own-property record. - return finish(`scr_domex_has_cause(${arg(0)})`); - case "error.domCause": - // +1 cause dyn value (the dyn undefined when absent). - return finish(`scr_domex_cause(${arg(0)})`); - case "error.domClone": - // WebIDL serialization: name/message copy, code re-derives. - // Borrowed receiver + borrowed options; +1 %DOMException. - // Option errors throw (may-throw seed set). - return finish(`scr_domex_clone(${arg(0)}, ${arg(1)})`); - case "regex.newChecked": return finish(`scr_regex_new_checked(${arg(0)}, ${arg(1)})`); - case "regex.new": - // Eager compile: bad patterns/flags throw catchable - // SyntaxError (may-throw seed set). Borrowed strings; +1. - return finish(`scr_regex_new(${arg(0)}, ${arg(1)})`); - // node:events EventEmitter (scr_events_emitter.c — linked - // exactly when these appear, moduleUsesEmitter). Receivers are - // borrowed and reinterpret to the runtime's ScrEmitter prefix; - // the chaining forms answer the receiver +1 (Node's `return - // this`), cast back to the receiver's static struct. - case "emitter.new": - // +1 bare emitter, collector-headered (the registry owns - // listener closures — unconditionally cycle-capable). - return finish(`scr_emitter_new()`); - case "emitter.ctor": - // super() into the prefix: the allocation already initialized - // it (registry NULL, display name stamped by the newFn). - return finish(`scr_emitter_init(${arg(0)})`); - case "emitter.on": { - // (recv, name, cb /moves/, once, prepend): the listener rides - // the registry with its emitted per-signature invoke adapter. - // May-throw: 'newListener' meta listeners run inside. - const cbT = e.args[2]!.type; - const cb = args[2]!; - emitter.moveTemp(cb); - const thunk = emitter.emitterInvokeThunkFor(cbT); - return finish( - `(${cType(e.type).trim()})scr_emitter_on((ScrEmitter *)${arg(0)}, ${arg(1)}, ${cb.name}, &${thunk}, ${arg(3)}, ${arg(4)})`, - ); - } - case "emitter.off": - // (recv, name, cb /borrowed — identity match/). May-throw: - // 'removeListener' meta listeners run inside. - return finish( - `(${cType(e.type).trim()})scr_emitter_off((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`, - ); - case "emitter.checkListener": - // Node's registration-family listener type check: a - // non-function dyn throws the catchable ERR_INVALID_ARG_TYPE - // TypeError (may-throw seed). - return finish(`scr_emitter_check_listener(${arg(0)})`); - case "emitter.onDyn": { - // (recv, name, cb /borrowed dyn — the identity/, adapter - // /moves/, once, prepend): the ADAPTER (the frontend's - // dynCheck value — it boxes the tuple to dyn and calls the - // original) rides the registry with its own invoke thunk; the - // runtime keeps the original as the entry's identity. - const adT = e.args[3]!.type; - const adapter = args[3]!; - emitter.moveTemp(adapter); - const dynThunk = emitter.emitterInvokeThunkFor(adT); - return finish( - `(${cType(e.type).trim()})scr_emitter_on_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${adapter.name}, &${dynThunk}, ${arg(4)}, ${arg(5)})`, - ); - } - case "emitter.onFlex": - return finish( - `(${cType(e.type).trim()})scr_emitter_on_flex((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`, - ); - case "emitter.onData": { - // The stream-'data' registration: same runtime entry as - // emitter.on, but the DATA invoke thunk (the two-slot payload - // ABI — see scr_stream_emit_data). - const cbT = e.args[2]!.type; - const cb = args[2]!; - emitter.moveTemp(cb); - const thunk = emitter.streamDataThunkFor(cbT); - return finish( - `(${cType(e.type).trim()})scr_emitter_on((ScrEmitter *)${arg(0)}, ${arg(1)}, ${cb.name}, &${thunk}, ${arg(3)}, ${arg(4)})`, - ); - } - case "emitter.onDataDyn": { - // The dyn-listener flavor: the frontend's adapter takes ONE - // dyn parameter; the data thunk boxes the payload by tag. - const adT = e.args[3]!.type; - const adapter = args[3]!; - emitter.moveTemp(adapter); - const dynThunk = emitter.streamDataThunkFor(adT); - return finish( - `(${cType(e.type).trim()})scr_emitter_on_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)}, ${adapter.name}, &${dynThunk}, ${arg(4)}, ${arg(5)})`, - ); - } - case "emitter.emitData": { - // A user emit('data', chunk) on a stream-rooted receiver: - // fill the matching payload slot, NULL the other. - const chunkT = e.args[2]!.type; - const both = - chunkT.kind === "string" - ? `(ScrBytes *)NULL, ${arg(2)}` - : `${arg(2)}, (ScrStr *)NULL`; - return finish(`scr_emitter_emit((ScrEmitter *)${arg(0)}, ${arg(1)}, ${both})`); - } - case "emitter.offDyn": - // (recv, name, cb /borrowed dyn/): identity is the dyn box's - // underlying closure. May-throw ('removeListener' meta). - return finish( - `(${cType(e.type).trim()})scr_emitter_off_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`, - ); - case "emitter.removeAll": - return finish( - `(${cType(e.type).trim()})scr_emitter_remove_all((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`, - ); - case "emitter.emit": - // The variadic dispatch: the event's unified tuple rides the - // C variadic tail, every argument borrowed (the invoke - // adapters retain each callee's +1). May-throw (listeners). - return finish( - `scr_emitter_emit((ScrEmitter *)${arg(0)}, ${arg(1)}${e.args.slice(2).map((_, i) => `, ${arg(i + 2)}`).join("")})`, - ); - case "emitter.emitFlex": { - const count = e.args.length - 2; - const payload = count === 0 - ? "NULL" - : `(ScrDyn *const[]){${e.args.slice(2).map((_, i) => arg(i + 2)).join(", ")}}`; - return finish( - `scr_emitter_emit_flex((ScrEmitter *)${arg(0)}, ${arg(1)}, ${payload}, ${count})`, - ); - } - case "emitter.emitError": - // emit('error', err): unhandled ⇒ the runtime throws the - // payload through the exception cell (may-throw seed). - return finish( - `scr_emitter_emit_error((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`, - ); - case "emitter.count": - return finish(`scr_emitter_listener_count((ScrEmitter *)${arg(0)}, ${arg(1)})`); - case "emitter.countFn": - return finish(`scr_emitter_listener_count_fn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "emitter.countDyn": - return finish(`scr_emitter_listener_count_dyn((ScrEmitter *)${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "emitter.names": - // +1 string[] in first-registration order. - return finish(`scr_emitter_event_names((ScrEmitter *)${arg(0)})`); - case "emitter.listeners": - // +1 closure array (originals — once wrappers are runtime- - // internal; SEMANTICS.md documents the rawListeners stance). - return finish(`scr_emitter_listeners((ScrEmitter *)${arg(0)}, ${arg(1)})`); - case "emitter.setMax": - return finish( - `(${cType(e.type).trim()})scr_emitter_set_max((ScrEmitter *)${arg(0)}, ${arg(1)})`, - ); - case "emitter.setMaxChk": - // The checked-dynamic ladder (may throw); +1 receiver back. - return finish( - `(${cType(e.type).trim()})scr_emitter_set_max_chk((ScrEmitter *)${arg(0)}, ${arg(1)})`, - ); - case "emitter.getMax": - return finish(`scr_emitter_get_max((ScrEmitter *)${arg(0)})`); - case "emitter.setDefaultMax": - return finish(`scr_emitter_set_default_max(${arg(0)})`); - case "emitter.setDefaultMaxChk": - return finish(`scr_emitter_set_default_max_chk(${arg(0)}, ${arg(1)})`); - case "emitter.getDefaultMax": - return finish(`scr_emitter_get_default_max()`); - case "error.code": { - // `string | undefined`, constructed type-directedly like - // process.envGet: a stamped code wraps the string arm (+1 - // moves into the box); absent yields the interned - // undefined-arm instance. The receiver may be a user subclass - // struct — the code slot sits in its ScrError prefix, so the - // cast is the ordinary upcast reinterpret. - if (e.type.kind !== "union") { - throw new InternalCompilerError("emitter bug: error.code result is not a union"); - } - const def = emitter.unionsById.get(e.type.unionId); - const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; - const undefTag = undefinedArmTag(e.type, emitter.unionsById); - if (strTag < 0 || undefTag < 0) { - throw new InternalCompilerError("emitter bug: error.code union lacks its arms"); - } - const s = emitter.newTemp(STRING, `scr_error_code((ScrError *)${arg(0)})`); - emitter.moveTemp(s); // moves into the box when present; NULL otherwise - const present = `scr_union_new_ref(${strTag}, ${s.name}, &scr_str_retain_v, &scr_str_release_v, NULL)`; - const absent = emitter.unitInstanceRef(e.type.unionId, undefTag); - return emitter.newTemp(e.type, `${s.name} ? ${present} : ${absent}`); - } - default: - throw new InternalCompilerError(`emitter bug: errorsEvents libCall dispatch for ${fn}`); - } -} - -function emitStreamLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "stream.onData": { - // The callback MOVES into the stream's listener registry; the - // adapter is per callback shape — runtime-provided for the - // zero-param and Buffer forms, emitted per union for the - // `Buffer | string` chunk (the chunk wraps at its Buffer arm). - emitter.usesTimers = true; // a flowing stream holds the loop - const cbT = e.args[1]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: stream.onData callback not a func"); - const cb = args[1]!; - emitter.moveTemp(cb); - const param = cbT.params[0]; - const adapter = - param === undefined - ? "scr_child_stream_thunk0" - : param.kind === "union" - ? emitter.childDataThunkFor(param) - : "scr_child_stream_thunk_bytes"; - emitter.line( - `scr_child_stream_on_data(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`, - ); - return { name: "", type: e.type }; - } - case "stream.onDataStr": { - emitter.usesTimers = true; - const cbT = e.args[1]!.type; - if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") { - throw new InternalCompilerError("emitter bug: stream.onDataStr callback not a string func"); - } - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line( - `scr_child_stream_on_data_str(${arg(0)}, ${cb.name}, &scr_child_stream_thunk_str, ${arg(2)});${emitter.srcComment(e.loc)}`, - ); - return { name: "", type: e.type }; - } - case "stream.onEnd": { - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_child_stream_on_end(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - case "stream.childSetEncoding": - return finish(`scr_child_stream_set_encoding(${arg(0)}, ${arg(1)})`); - // node:stream (scr_stream.c — linked exactly when these appear, - // moduleUsesStream). Receivers reinterpret to the shared - // ScrStream layout (the ScrEmitter prefix plus the stream-state - // pointer); chaining forms answer the receiver +1 cast back to - // its static class. Any operation that can run user code holds - // the loop open (deferred next-tick emissions): usesTimers. - case "readable.new": - case "writable.new": - case "duplex.new": - case "transform.new": - case "passthrough.new": - case "readable.init": - case "writable.init": - case "duplex.init": - case "transform.init": - case "passthrough.init": { - emitter.usesTimers = true; - // Head args then flags then the PRESENT callbacks in canonical - // order — the flags literal names which; absent ones emit NULL. - // The .init forms (a subclass constructor's super(options)) - // carry the BORROWED receiver at arg 0 and shift everything by - // one; scr_stream_init_* fills the already-allocated struct. - const base = fn.slice(0, fn.indexOf(".")); - const isInit = fn.endsWith(".init"); - const off = isInit ? 1 : 0; - const duplexShape = base !== "readable" && base !== "writable"; - const headLen = duplexShape ? 8 : 4; - const flagsArg = e.args[off + headLen - 1]!; - if (flagsArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} flags not a literal`); - const flags = flagsArg.value; - const canonical: { name: string; kind: "r" | "w" | "f" | "d" | "t" | "l" }[] = - base === "readable" - ? [{ name: "read", kind: "r" }, { name: "destroy", kind: "d" }] - : base === "writable" - ? [{ name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }] - : base === "duplex" - ? [{ name: "read", kind: "r" }, { name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }] - : [{ name: "transform", kind: "t" }, { name: "flush", kind: "l" }, { name: "destroy", kind: "d" }]; - const cbArgs: string[] = []; - let at = off + headLen; - for (let i = 0; i < canonical.length; i++) { - if ((flags & (1 << i)) === 0) { - cbArgs.push("NULL", "NULL"); - continue; - } - const cb = args[at]!; - const cbT = e.args[at]!.type; - emitter.moveTemp(cb); // the callback closure MOVES into the stream - cbArgs.push(cb.name, `&${emitter.streamCbThunkFor(canonical[i]!.kind, cbT)}`); - at++; - } - const entry = isInit ? `scr_stream_init_${base}` : `scr_stream_new_${base}`; - const headIdx = duplexShape ? [0, 1, 2, 3, 4, 5, 6] : [0, 1, 2]; - const head = headIdx.map((i) => arg(off + i)).join(", "); - if (isInit) { - return finish(`${entry}((ScrStream *)${arg(0)}, ${head}, ${cbArgs.join(", ")})`); - } - return finish(`(${cType(e.type).trim()})${entry}(${head}, ${cbArgs.join(", ")})`); - } - case "stream.setRead": - case "stream.setWrite": - case "stream.setFinal": - case "stream.setDestroy": - case "stream.setTransform": - case "stream.setFlush": { - // The underscore-method assignment surface: the runtime slot - // swaps its closure (+1 moves in) and invoke thunk — the next - // _read/_write/... dispatch uses it, Node's timing. - emitter.usesTimers = true; - const kindOf: Record = { - "stream.setRead": "r", "stream.setWrite": "w", "stream.setFinal": "f", - "stream.setDestroy": "d", "stream.setTransform": "t", "stream.setFlush": "l", - }; - const symOf: Record = { - "stream.setRead": "scr_stream_set_read", "stream.setWrite": "scr_stream_set_write", - "stream.setFinal": "scr_stream_set_final", "stream.setDestroy": "scr_stream_set_destroy", - "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", - }; - const cb = args[1]!; - const cbT = e.args[1]!.type; - emitter.moveTemp(cb); // the callback closure MOVES into the stream - return finish(`${symOf[fn]!}((ScrStream *)${arg(0)}, ${cb.name}, &${emitter.streamCbThunkFor(kindOf[fn]!, cbT)})`); - } - case "stream.finished": - case "stream.finishedDyn": { - // finished(s, cb): the +1 cleanup closure answers. Typed - // callbacks ride the "e" thunk (this + Error|null prefix); - // dyn values ride the runtime's own inv. - emitter.usesTimers = true; - if (fn === "stream.finishedDyn") { - return finish(`(${cType(e.type).trim()})scr_stream_finished_dyn((ScrStream *)${arg(0)}, ${arg(1)})`); - } - const cb = args[1]!; - emitter.moveTemp(cb); // the watcher closure MOVES into the stream - const thunk = emitter.streamCbThunkFor("e", e.args[1]!.type); - return finish(`(${cType(e.type).trim()})scr_stream_finished((ScrStream *)${arg(0)}, ${cb.name}, &${thunk})`); - } - case "stream.pipeline": - case "stream.pipelineDyn": { - // pipeline(count, s1..sn, cb): the destination answers +1. - emitter.usesTimers = true; - const countArg = e.args[0]!; - if (countArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} count not a literal`); - const n = countArg.value; - const list = Array.from({ length: n }, (_, i) => `(ScrStream *)${arg(1 + i)}`).join(", "); - if (fn === "stream.pipelineDyn") { - return finish( - `(${cType(e.type).trim()})scr_stream_pipeline_dyn(${n}, (ScrStream *[]){ ${list} }, ${arg(1 + n)})`, - ); - } - const cb = args[1 + n]!; - emitter.moveTemp(cb); - const thunk = emitter.streamCbThunkFor("e", e.args[1 + n]!.type); - return finish( - `(${cType(e.type).trim()})scr_stream_pipeline(${n}, (ScrStream *[]){ ${list} }, ${cb.name}, &${thunk})`, - ); - } - case "sc.text": - // stream/consumers: +1 pending promises the accumulate-and- - // settle machinery resolves or rejects (the stream's - // error / premature close / json's SyntaxError). - emitter.usesTimers = true; - return finish(`scr_sc_text((ScrStream *)${arg(0)})`); - case "sc.json": - emitter.usesTimers = true; - return finish(`scr_sc_json((ScrStream *)${arg(0)})`); - case "sc.buffer": - emitter.usesTimers = true; - return finish(`scr_sc_buffer((ScrStream *)${arg(0)})`); - case "readable.newDyn": - case "writable.newDyn": - case "duplex.newDyn": - case "transform.newDyn": - case "passthrough.newDyn": { - // The dyn-options constructor: the runtime walks the record - // (borrowed). MAY THROW (NULL result with the exception - // pending — the seed set carries these). - emitter.usesTimers = true; - const base = fn.slice(0, fn.indexOf(".")); - return finish(`(${cType(e.type).trim()})scr_stream_new_${base}_dyn(${arg(0)})`); - } - case "readable.initDyn": - case "writable.initDyn": - case "duplex.initDyn": - case "transform.initDyn": - case "passthrough.initDyn": { - // The dyn-options super(options): borrowed receiver + record, - // then the FALLBACK underscore-method wrappers in canonical - // order (flags names which; absent ones emit NULL — exactly - // the .init callback ABI; wrappers MOVE). - emitter.usesTimers = true; - const base = fn.slice(0, fn.indexOf(".")); - const flagsArg = e.args[2]!; - if (flagsArg.kind !== "numLit") throw new InternalCompilerError(`emitter bug: ${fn} flags not a literal`); - const flags = flagsArg.value; - const canonical: { name: string; kind: "r" | "w" | "f" | "d" | "t" | "l" }[] = - base === "readable" - ? [{ name: "read", kind: "r" }, { name: "destroy", kind: "d" }] - : base === "writable" - ? [{ name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }] - : base === "duplex" - ? [{ name: "read", kind: "r" }, { name: "write", kind: "w" }, { name: "final", kind: "f" }, { name: "destroy", kind: "d" }] - : [{ name: "transform", kind: "t" }, { name: "flush", kind: "l" }, { name: "destroy", kind: "d" }]; - const cbArgs: string[] = []; - let at = 3; - for (let i = 0; i < canonical.length; i++) { - if ((flags & (1 << i)) === 0) { - cbArgs.push("NULL", "NULL"); - continue; - } - const cb = args[at]!; - const cbT = e.args[at]!.type; - emitter.moveTemp(cb); - cbArgs.push(cb.name, `&${emitter.streamCbThunkFor(canonical[i]!.kind, cbT)}`); - at++; - } - return finish(`scr_stream_init_${base}_dyn((ScrStream *)${arg(0)}, ${arg(1)}, ${cbArgs.join(", ")})`); - } - case "readable.push": - emitter.usesTimers = true; - return finish(`scr_stream_push((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.pushStr": - emitter.usesTimers = true; - return finish(`scr_stream_push_str((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.pushStrEnc": - // push(chunk, enc) with a literal non-utf8 encoding. - emitter.usesTimers = true; - return finish(`scr_stream_push_str_enc((ScrStream *)${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "readable.pushEncoding": - // The defaultEncoding option's push side (chaining, +1). - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_set_push_encoding((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.pushNull": - emitter.usesTimers = true; - return finish(`scr_stream_push_null((ScrStream *)${arg(0)})`); - case "readable.pushU": - case "writable.writeU": { - // Union-typed chunk: dispatch by tag (bytes / string / null - // arms — the frontend admitted exactly those). - emitter.usesTimers = true; - const t = e.args[1]!.type; - if (t.kind !== "union") throw new InternalCompilerError(`emitter bug: ${fn} chunk not a union`); - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: ${fn} union unknown`); - const bytesTag = def.arms.findIndex((a) => a.kind === "bytes"); - const strTag = def.arms.findIndex((a) => a.kind === "string"); - const nullTag = def.arms.findIndex((a) => a.kind === "nullT"); - const u = arg(1); - const recv = `(ScrStream *)${arg(0)}`; - const pushing = fn === "readable.pushU"; - const onBytes = pushing - ? `scr_stream_push(${recv}, (ScrBytes *)scr_union_peek(${u}))` - : `scr_stream_write(${recv}, (ScrBytes *)scr_union_peek(${u}), NULL)`; - const onStr = pushing - ? `scr_stream_push_str(${recv}, (ScrStr *)scr_union_peek(${u}))` - : `scr_stream_write_str(${recv}, (ScrStr *)scr_union_peek(${u}), NULL)`; - const onNull = pushing - ? `scr_stream_push_null(${recv})` - : `scr_stream_write_null(${recv})`; - const present: { tag: number; expr: string }[] = [ - ...(nullTag >= 0 ? [{ tag: nullTag, expr: onNull }] : []), - ...(strTag >= 0 ? [{ tag: strTag, expr: onStr }] : []), - ...(bytesTag >= 0 ? [{ tag: bytesTag, expr: onBytes }] : []), - ]; - if (present.length === 0) throw new InternalCompilerError(`emitter bug: ${fn} union lacks its arms`); - const tail = present[present.length - 1]!.expr; - const chain = present.slice(0, -1).map((a) => `${u}->tag == ${a.tag} ? ${a.expr}`); - return finish(chain.length > 0 ? `(${chain.join(" : ")} : ${tail})` : tail); - } - case "readable.pushDyn": - emitter.usesTimers = true; - return finish(`scr_stream_push_dyn((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.unshift": - emitter.usesTimers = true; - return finish(`scr_stream_unshift((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.unshiftStr": - emitter.usesTimers = true; - return finish(`scr_stream_unshift_str((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.read": { - // +1 Buffer or NULL → the `Buffer | null` union, constructed - // type-directedly (the error.code pattern). - emitter.usesTimers = true; - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: readable.read result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (bytesTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: readable.read union lacks its arms"); - const b = emitter.newTemp(bytesOf("u8"), `scr_stream_read((ScrStream *)${arg(0)}, ${arg(1)})`); - emitter.emitPendingCheck(); - emitter.moveTemp(b); // moves into the union arm when present - const present = `scr_union_new_ref(${bytesTag}, ${b.name}, &scr_bytes_retain_v, &scr_bytes_release_v, NULL)`; - return emitter.newTemp(e.type, `${b.name} ? ${present} : scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)})`); - } - case "readable.pause": - return finish(`(${cType(e.type).trim()})scr_stream_pause((ScrStream *)${arg(0)})`); - case "readable.setEncoding": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_set_encoding((ScrStream *)${arg(0)}, ${arg(1)})`); - case "readable.nextChunk": - // +1 promise; may run the user _read synchronously (may-throw). - emitter.usesTimers = true; - return finish(`scr_stream_next_chunk((ScrStream *)${arg(0)})`); - case "readable.nextChunkDyn": - emitter.usesTimers = true; - return finish(`scr_stream_next_chunk_dyn((ScrStream *)${arg(0)})`); - case "readable.fromArr": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_from_arr(${arg(0)}, ${arg(1)})`); - case "readable.resume": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_resume((ScrStream *)${arg(0)})`); - case "readable.isPaused": - return finish(`scr_stream_is_paused((ScrStream *)${arg(0)})`); - case "readable.pipe": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_pipe((ScrStream *)${arg(0)}, (ScrStream *)${arg(1)}, ${arg(2)})`); - case "readable.unpipe": - emitter.usesTimers = true; - return finish( - e.args.length > 1 - ? `(${cType(e.type).trim()})scr_stream_unpipe((ScrStream *)${arg(0)}, (ScrStream *)${arg(1)})` - : `(${cType(e.type).trim()})scr_stream_unpipe((ScrStream *)${arg(0)}, NULL)`, - ); - case "readable.flowing": { - // -1 (null: never kicked) / 0 / 1 → the `boolean | null` union. - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: readable.flowing result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const boolTag = def ? def.arms.findIndex((a) => a.kind === "bool") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (boolTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: readable.flowing union lacks its arms"); - const f = emitter.newTemp({ kind: "f64" }, `scr_stream_flowing((ScrStream *)${arg(0)})`); - return emitter.newTemp( - e.type, - `${f.name} < 0 ? scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)}) : scr_union_new_bool(${boolTag}, ${f.name} > 0)`, - ); - } - case "writable.writeDyn": - emitter.usesTimers = true; - return finish(`scr_stream_write_dyn((ScrStream *)${arg(0)}, ${arg(1)}, NULL)`); - case "writable.write": - case "writable.writeStr": { - emitter.usesTimers = true; - const entry = fn === "writable.write" ? "scr_stream_write" : "scr_stream_write_str"; - if (e.args.length > 2) { - const cb = args[2]!; - emitter.moveTemp(cb); // the user's completion callback MOVES - return finish(`${entry}((ScrStream *)${arg(0)}, ${arg(1)}, ${cb.name})`); - } - return finish(`${entry}((ScrStream *)${arg(0)}, ${arg(1)}, NULL)`); - } - case "writable.end": { - // (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string - // chunk, 4 callback. - emitter.usesTimers = true; - const flagsArg = e.args[1]!; - if (flagsArg.kind !== "numLit") throw new InternalCompilerError("emitter bug: writable.end flags not a literal"); - const flags = flagsArg.value; - let at = 2; - let chunkB = "NULL"; - let chunkS = "NULL"; - let chunkD: string | null = null; - if (flags & 1) chunkB = arg(at++); - else if (flags & 2) chunkS = arg(at++); - else if (flags & 8) chunkD = arg(at++); // dyn chunk: write first - let cbName = "NULL"; - if (flags & 4) { - const cb = args[at]!; - emitter.moveTemp(cb); - cbName = cb.name; - } - const endCall = `(${cType(e.type).trim()})scr_stream_end((ScrStream *)${arg(0)}, ${chunkB}, ${chunkS}, ${cbName})`; - if (chunkD !== null) { - // end(chunk) with a checked-dynamic chunk: write, then end - // (exactly Node's end(chunk) decomposition). - return finish(`(scr_stream_write_dyn((ScrStream *)${arg(0)}, ${chunkD}, NULL), ${endCall})`); - } - return finish(endCall); - } - case "writable.cork": - return finish(`scr_stream_cork((ScrStream *)${arg(0)})`); - case "writable.uncork": - emitter.usesTimers = true; - return finish(`scr_stream_uncork((ScrStream *)${arg(0)})`); - case "stream.destroy": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_destroy((ScrStream *)${arg(0)}, NULL)`); - case "stream.destroyErr": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_destroy((ScrStream *)${arg(0)}, ${arg(1)})`); - case "stream.iteratorClose": - emitter.usesTimers = true; - return finish(`(${cType(e.type).trim()})scr_stream_iterator_close((ScrStream *)${arg(0)})`); - case "stream.prop": { - // The property NAME is a compile-time literal; args[1]'s - // emitted temp is unused (released with the statement's frame). - const nameArg = e.args[1]!; - if (nameArg.kind !== "strLit") throw new InternalCompilerError("emitter bug: stream.prop name not a literal"); - const c = `scr_stream_prop((ScrStream *)${arg(0)}, "${nameArg.value}")`; - return finish(e.type.kind === "bool" ? `(${c} != 0)` : c); - } - case "stream.errored": { - // +1 error or NULL → the `Error | null` union (error.code's - // construction pattern). - if (e.type.kind !== "union") throw new InternalCompilerError("emitter bug: stream.errored result is not a union"); - const def = emitter.unionsById.get(e.type.unionId); - const errTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1; - const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1; - if (errTag < 0 || nullTag < 0) throw new InternalCompilerError("emitter bug: stream.errored union lacks its arms"); - const er = emitter.newTemp({ kind: "object", className: "%Error" }, `scr_stream_errored((ScrStream *)${arg(0)})`); - emitter.moveTemp(er); - const present = `scr_union_new_ref(${errTag}, ${er.name}, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg())`; - return emitter.newTemp(e.type, `${er.name} ? ${present} : scr_union_retain(${emitter.unitInstanceRef(e.type.unionId, nullTag)})`); - } - default: - throw new InternalCompilerError(`emitter bug: stream libCall dispatch for ${fn}`); - } -} - -function emitAssertInspectLibCall(state: LibCallState): Temp { - const { e, arg, finish } = state; - const fn = e.fn; - switch (fn) { - case "assert.expectsErrDyn": - return finish(`scr_assert_expects_err_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - // node:assert (scr_assert.c; match in scr_regex.c): all args - // borrowed; failures throw the catchable AssertionError (the - // may-throw seed runs the pending check after each). - case "assert.ok": - return finish(`scr_assert_ok(${arg(0)}, ${arg(1)})`); - case "assert.eqF64": - return finish(`scr_assert_eq_f64(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.eqBigInt": - return finish(`scr_assert_eq_bigint(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.eqStr": - return finish(`scr_assert_eq_str(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.eqBool": - return finish(`scr_assert_eq_bool(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.eqSym": - // Symbol strict equality: pointer identity, v24's - // "Symbol(desc)" stacked-diff messages (scr_symbol.c). - return finish(`scr_assert_eq_sym(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.eqDyn": - // The quartet over checked-dynamic operands: SameValue / - // dyn-walk deep equality, assertion_error.js messages. - return finish(`scr_assert_eq_dyn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.looseResult": - return finish(`scr_assert_loose_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.deepResult": - return finish(`scr_assert_deep_result(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "assert.deqEnter": - return finish(`scr_assert_deq_enter(${arg(0)}, ${arg(1)})`); - case "assert.deqLeave": - return finish(`scr_assert_deq_leave()`); - case "assert.sameValue": - return finish(`scr_assert_same_value_f64(${arg(0)}, ${arg(1)})`); - case "assert.match": - return finish(`scr_assert_match(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "assert.throwsNone": - return finish(`scr_assert_throws_none(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "assert.throwsMismatch": - return finish(`scr_assert_throws_mismatch(${arg(0)}, (ScrError *)${arg(1)}, ${arg(2)}, ${arg(3)})`); - case "assert.throwsRegex": - // assert.throws(fn, /re/): Node tests String(error) - // (scr_regex.c — the regex argument keeps the link switch on). - return finish(`scr_assert_throws_regex(${arg(0)}, (ScrError *)${arg(1)}, ${arg(2)}, ${arg(3)})`); - // The throws(fn, {shape}) accumulator: begin/slot calls never - // throw; shapeEnd throws the byte-exact Comparison diff on - // mismatch. - case "assert.shapeBegin": - return finish(`scr_assert_shape_begin((ScrError *)${arg(0)})`); - case "assert.shapeStr": - return finish(`scr_assert_shape_str(${arg(0)}, ${arg(1)})`); - case "assert.shapeRe": - return finish(`scr_assert_shape_re(${arg(0)}, ${arg(1)})`); - case "assert.shapeEnd": - return finish(`scr_assert_shape_end(${arg(0)}, ${arg(1)})`); - case "assert.regexErrTest": - return finish(`scr_assert_regex_err_test(${arg(0)}, (ScrError *)${arg(1)})`); - case "assert.unwantedRejection": - return finish(`scr_assert_unwanted_rejection((ScrError *)${arg(0)}, ${arg(1)}, ${arg(2)})`); - // assert.ifError's per-type throws (always throw — Node rejects - // falsy values too; null/undefined never reach the runtime). - case "assert.ifErrorErr": - return finish(`scr_assert_iferror_err((ScrError *)${arg(0)})`); - case "assert.ifErrorF64": - return finish(`scr_assert_iferror_f64(${arg(0)})`); - case "assert.ifErrorStr": - return finish(`scr_assert_iferror_str(${arg(0)})`); - case "assert.ifErrorBool": - return finish(`scr_assert_iferror_bool(${arg(0)})`); - case "assert.ifErrorDyn": - // The checked-dynamic argument (test/common's mustSucceed): - // units pass quietly, %error-marked objects throw with the - // error's message, everything else with the inspection. - return finish(`scr_assert_iferror_dyn(${arg(0)})`); - case "assert.refEqBytes": - // strictEqual/notStrictEqual over bytes: pointer identity, - // Node's object-comparison headers (brands_eq is the static - // brand agreement — it shapes only the failure header). - return finish(`scr_assert_ref_eq_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - case "assert.refEqFn": - // strictEqual/notStrictEqual over functions: pointer identity, - // the object-comparison headers. - return finish(`scr_assert_ref_eq_fn(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)})`); - case "assert.bytesDeepEq": - // deepStrictEqual's verdict: static brand agreement AND - // content equality. Never throws. - return finish(`scr_assert_bytes_deep_eq(${arg(0)}, ${arg(1)}, ${arg(2)})`); - // util.inspect (scr_inspect.c): all args borrowed; string - // results +1. Only insp.jsval throws (composite island values — - // the may-throw seed runs the pending check). - case "insp.f64": - return finish(`scr_insp_f64(${arg(0)})`); - case "insp.jsonDyn": - return finish(`scr_dyn_format_j(${arg(0)})`); - case "insp.str": - return finish(`scr_insp_str(${arg(0)})`); - case "insp.regex": - return finish(`scr_insp_regex(${arg(0)})`); - case "insp.buffer": - return finish(`scr_insp_buffer(${arg(0)})`); - case "insp.error": - // The receiver may be a user subclass struct — its ScrError - // prefix carries the name/message/code slots (the error.code - // precedent). - return finish(`scr_insp_error((ScrError *)${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "insp.dyn": - return finish(`scr_insp_dyn(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "insp.dynS": - return finish(`scr_insp_dyn_s(${arg(0)}, ${arg(1)})`); - case "insp.jsval": - return finish(`scr_insp_jsval(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "insp.begin": - return finish(`scr_insp_begin(${arg(0)})`); - // Circular references over cycle-capable composites: check/push/ - // wrap drive Node's seen/circular machinery (scr_inspect.c). - case "insp.circCheck": - return finish(`scr_insp_circ_check(${arg(0)})`); - case "insp.seenPush": - return finish(`scr_insp_seen_push(${arg(0)})`); - case "insp.refWrap": - return finish(`scr_insp_ref_wrap(${arg(0)}, ${arg(1)})`); - case "insp.circular": - return finish(`scr_insp_circular(${arg(0)})`); - case "insp.entry": - return finish(`scr_insp_entry(${arg(0)}, ${arg(1)})`); - case "insp.key": - return finish(`scr_insp_key(${arg(0)})`); - case "insp.moreItems": - return finish(`scr_insp_more_items(${arg(0)})`); - case "insp.end": - return finish(`scr_insp_end(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)})`); - default: - throw new InternalCompilerError(`emitter bug: assertInspect libCall dispatch for ${fn}`); - } -} - -function emitIoLibCall(state: LibCallState): Temp { - const { e, emitter, args, arg, finish } = state; - const fn = e.fn; - switch (fn) { - // node:readline (scr_readline.c, the events gate): an OPEN - // interface is a stdin consumer — the loop must run. - case "rl.create": - emitter.usesTimers = true; - return finish(`scr_rl_create()`); - case "rl.question": { - emitter.usesTimers = true; - const cbT = e.args[2]!.type; - if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: rl.question callback not a func"); - const cb = args[2]!; - emitter.moveTemp(cb); - const adapter = - cbT.params.length === 0 ? "scr_rl_answer_thunk0" : "scr_rl_answer_thunk_str"; - // Throws Node's use-after-close error; the pending check comes - // from the may-throw seed. - emitter.line(`scr_rl_question(${arg(0)}, ${arg(1)}, ${cb.name}, &${adapter});${emitter.srcComment(e.loc)}`); - emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - case "rl.close": - return finish(`scr_rl_close(${arg(0)})`); - case "rl.onClose": { - emitter.usesTimers = true; - const cb = args[1]!; - emitter.moveTemp(cb); - emitter.line(`scr_rl_on_close(${arg(0)}, ${cb.name});${emitter.srcComment(e.loc)}`); - return { name: "", type: e.type }; - } - // The StringDecoder trio (scr_bytes.c): pure functions over the - // canonical encoding name + packed-f64 pending state; never - // throw. - case "strdec.write": - return finish(`scr_strdec_write(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "strdec.next": - return finish(`scr_strdec_next(${arg(0)}, ${arg(1)}, ${arg(2)})`); - case "strdec.end": - return finish(`scr_strdec_end(${arg(0)}, ${arg(1)})`); - default: - throw new InternalCompilerError(`emitter bug: io libCall dispatch for ${fn}`); - } -} - -function emitLibCallExpr(emitter: CEmitter, e: LibCallExpr): Temp { - const packed = e.args[0]; - if (e.fn === "string.fromCharCode" && packed?.kind === "arrayLit" && - packed.elems.length === 1 && !packed.spreads?.length) { - // The literal is only the variadic ABI pack, never an observable - // array. Evaluate the scalar once before bypassing that allocation. - const code = emitter.emitExpr(packed.elems[0]!); - return emitter.newTemp(e.type, `scr_str_from_char_code_one(${code.name})`); - } - // Standard-library call. Args are BORROWED (owned temps of the - // current frame, released at statement end); refcounted results come - // back +1 (process.argv: +1 on the runtime's ONE interned array — - // JS identity — everything else fresh). Throwing members (the - // may-throw seed set) get the standard pending check, emitted after - // a result temp joins its frame so an unwind releases it. - const args = e.args.map((a) => emitter.emitExpr(a)); - const arg = (i: number) => args[i]!.name; - const finish = (call: string): Temp => { - if (e.type.kind === "void") { - emitter.line(`${call};${emitter.srcComment(e.loc)}`); - if (MAY_THROW_LIB_FNS.has(e.fn)) emitter.emitPendingCheck(); - return { name: "", type: e.type }; - } - const t = emitter.newTemp(e.type, call); - if (MAY_THROW_LIB_FNS.has(e.fn)) emitter.emitPendingCheck(); - return t; - }; - const fn = e.fn; - const state: LibCallState = { emitter, e, args, arg, finish }; - const prefix = fn.slice(0, fn.indexOf(".")) as LibCallPrefix; - switch (prefix) { - case "fetch": - case "island": - case "json": - return emitWebLibCall(state); - case "weakSet": - case "weakMap": - case "ffi": - case "arrayBuffer": - case "dyn": - case "global": - return emitDynamicLibCall(state); - case "fs": - case "fsp": - case "fileHandle": - case "watcher": - case "stats": - case "zlib": - case "atomics": - return emitFilesystemLibCall(state); - case "path": - case "os": - case "url": - case "sp": - case "qs": - return emitPathUrlLibCall(state); - case "math": - case "num": - case "str": - case "regexp": - case "intl": - case "sym": - case "bigint": - case "perf": - case "number": - case "date": - case "text": - case "string": - case "class": - return emitPrimitiveLibCall(state); - case "util": - return emitUtilLibCall(state); - case "crypto": - case "buffer": - case "bytes": - return emitCryptoBytesLibCall(state); - case "cp": - case "spawnRes": - case "child": - case "writer": - case "procStream": - return emitChildProcessLibCall(state); - case "net": - case "dgram": - case "dns": - return emitNetworkLibCall(state); - case "test": - return emitTestLibCall(state); - case "http": - case "https": - return emitHttpLibCall(state); - case "tls": - case "tlsca": - return emitTlsLibCall(state); - case "http2": - return emitHttp2LibCall(state); - case "tp": - case "dc": - case "timers": - case "async": - case "als": - return emitAsyncContextLibCall(state); - case "process": - case "stdin": - case "module": - return emitProcessLibCall(state); - case "error": - case "regex": - case "emitter": - return emitErrorsEventsLibCall(state); - case "stream": - case "readable": - case "writable": - case "duplex": - case "transform": - case "passthrough": - case "sc": - return emitStreamLibCall(state); - case "assert": - case "insp": - return emitAssertInspectLibCall(state); - case "rl": - case "strdec": - return emitIoLibCall(state); - default: { - const _exhaustive: never = prefix; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -export function emitExpr(emitter: CEmitter, e: IrExpr): Temp { - switch (e.kind) { - case "numLit": - case "boolLit": - case "strLit": - case "moduleNsRef": - case "unitLit": - case "varRef": - return emitLiteralExpr(emitter, e); - case "bin": - case "unary": - case "incDec": - case "fieldIncDec": - case "assignExpr": - case "seqExpr": - return emitOperatorExpr(emitter, e); - case "dynDestrCheck": - case "dynIterN": - case "toBool": - case "logical": - case "ternary": - case "optChain": - case "chainRecv": - case "orDefault": - case "nullish": - return emitControlExpr(emitter, e); - case "strConcat": - case "strEq": - case "strCmp": - case "toString": - case "strIntrinsic": - case "regexLit": - case "templateStrings": - case "regexIntrinsic": - return emitStringExpr(emitter, e); - case "arrayLit": - case "arrayNewLen": - case "arrayGet": - case "arrayHas": - case "arrayState": - case "arrIntrinsic": - case "bytesNew": - case "bytesIntrinsic": - case "mapNew": - case "mapIntrinsic": - case "setIntrinsic": - case "setNew": - return emitContainerExpr(emitter, e); - case "call": - case "ffiCall": - case "closure": - case "callValue": - case "selfRef": - case "new": - case "classRef": - case "newValue": - case "instanceOfValue": - case "promiseVoidWiden": - case "upcast": - case "downcast": - case "instanceOf": - case "virtualCall": - return emitCallExpr(emitter, e); - case "fieldGet": - case "recordGet": - case "recordLit": - case "recordClone": - case "recordKeyGet": - case "recordOvfKeys": - case "recordOvfHas": - return emitRecordExpr(emitter, e); - case "dynFrom": - case "dynFromJsval": - case "dynCall": - case "dynInvoke": - case "dynArrLit": - case "dynObjLit": - case "unionWrap": - case "unionNarrow": - case "unionDisc": - case "unionKeyGet": - case "unionIsTag": - case "dynKeyGet": - case "dynHasKey": - case "dynScalarEq": - case "dynTest": - case "unionEq": - case "unionFuncEq": - case "caughtTest": - case "caughtCheck": - case "caughtNarrow": - case "caughtToDyn": - return emitDynamicExpr(emitter, e); - case "intrinsic": - return emitIntrinsicExpr(emitter, e); - case "jsonStringify": - case "dynCheck": - return emitSerializationExpr(emitter, e); - case "yieldExpr": - case "genResume": - case "awaitExpr": - case "awaitUnionExpr": - case "newPromise": - case "promiseWithResolvers": - return emitAsyncExpr(emitter, e); - case "jsMarshal": - case "jsOp": - case "jsExit": - case "jsBridgePromise": - return emitJsInteropExpr(emitter, e); - case "libCall": - return emitLibCallExpr(emitter, e); - default: { - const _exhaustive: never = e; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} diff --git a/packages/compiler/src/backend/c/island.ts b/packages/compiler/src/backend/c/island.ts deleted file mode 100644 index c2c332181..000000000 --- a/packages/compiler/src/backend/c/island.ts +++ /dev/null @@ -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(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; - } diff --git a/packages/compiler/src/backend/c/shapes.ts b/packages/compiler/src/backend/c/shapes.ts deleted file mode 100644 index e6f533916..000000000 --- a/packages/compiler/src/backend/c/shapes.ts +++ /dev/null @@ -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; - } - } - -/** `&` 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)})`; - } diff --git a/packages/compiler/src/backend/c/stmts.ts b/packages/compiler/src/backend/c/stmts.ts deleted file mode 100644 index f344df8fb..000000000 --- a/packages/compiler/src/backend/c/stmts.ts +++ /dev/null @@ -1,1090 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; -/* Statement-level C emission: function bodies, blocks/scopes, the statement - * dispatch (emitStmt), try/catch lowering, and switch — plus the small - * branch/condition helpers they share with expression emission. All frame, - * scope, and temp state lives on CEmitter; these functions drive it. */ -import type { CEmitter, ScopeEntry } from "./c-emitter.js"; -import type { IrFunction } from "../../ir/ir.js"; -import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangle.js"; -import { BOOL, CAUGHT, type IrExpr, type IrStmt, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/ir.js"; -import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./types.js"; -import { OVERFLOW_MEMBER } from "./shapes.js"; -import { checkGlobalTdz, writeBox } from "./bindings.js"; -import { emitStableReceiver } from "./exprs.js"; -import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js"; -import { analyzeIntegerRanges } from "../../ir/integer-ranges.js"; -import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js"; - - - - - - -export function emitFunction(emitter: CEmitter, fn: IrFunction): void { - emitter.sourceLoc = fn.loc; - emitter.tempCounter = 0; - emitter.frames = []; - emitter.scopes = []; - emitter.jumpTargets = []; - emitter.tryStack = []; - emitter.finallyStack = []; - emitter.pendingReturnScopeIndex = null; - emitter.currentReturnType = fn.returnType; - emitter.currentGenerator = fn.generator ?? null; - emitter.labelCounter = 0; - emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l])); - emitter.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId)); - emitter.integerLoopBindings.clear(); - emitter.integerRanges = analyzeIntegerRanges(fn); - - emitter.line(`${emitter.signature(fn)} {${emitter.srcComment(fn.loc)}`); - emitter.indent++; - - // The pending-return slot: a `return` crossing a finally computes its - // value FIRST (before the finally runs — snapshotting it here is what - // makes finally mutations of returned locals invisible, Node-exact), - // then jumps through every crossed finally; the last dispatch returns - // the slot. Declared only when some return actually crosses a finally. - if (fn.returnType.kind !== "void" && returnCrossesFinally(fn.body)) { - const init = isRefCounted(fn.returnType) ? "NULL" : "0"; - emitter.line(`${cDecl(fn.returnType, "sc_pret")} = ${init}; /* pending return (through finally) */`); - } - - // Captured bindings come in through the environment — borrowed for the - // whole call (the closure owns them): bound here, never released here. - (fn.captures ?? []).forEach((c, i) => { - emitter.line(`ScrBox *${mangleLocal(c.localId)} = sc_env->caps[${i}]; /* captured ${c.name} */`); - }); - (fn.classCaptures ?? []).forEach((c) => { - const self = fn.params[0]!; - const local = emitter.currentLocals.get(self.localId)!; - const receiver = local.boxed ? mangleRawParam(self.localId) : mangleLocal(self.localId); - emitter.line(`ScrBox *${mangleLocal(c.localId)} = ${receiver}->sc_class->caps[${c.slot}]; /* captured ${c.name} */`); - }); - - const paramIds = new Set(fn.params.map((p) => p.localId)); - for (const local of fn.locals) { - if (paramIds.has(local.id) || emitter.captureIds.has(local.id)) continue; - if (local.boxed) { - emitter.line(`ScrBox *${mangleLocal(local.id)} = NULL; /* ${local.name} (boxed) */`); - } else { - const init = isRefCounted(local.type) ? " = NULL" : ""; - emitter.line(`${cDecl(local.type, mangleLocal(local.id))}${init}; /* ${local.name} */`); - } - } - - // Function scope owns refcounted params (callees own their params). - // Boxed params: allocate the shared binding and move the raw value in. - const fnScope: ScopeEntry[] = []; - for (const p of fn.params) { - const local = emitter.currentLocals.get(p.localId)!; - if (local.boxed) { - const box = mangleLocal(p.localId); - emitter.line(`ScrBox *${box} = ${emitter.boxNewC(p.type)}; /* ${p.name} (boxed param) */`); - emitter.line(`scr_box_set_${boxAccess(p.type)}(${box}, ${mangleRawParam(p.localId)});`); - fnScope.push({ name: box, type: p.type, boxed: true }); - } else if (isRefCounted(p.type)) { - fnScope.push({ name: mangleLocal(p.localId), type: p.type }); - } - } - emitter.scopes.push(fnScope); - emitter.emitStmts(fn.body); - // Implicit exit of a void function: release function-scope refcounted - // locals (unless the body already ended in an explicit return or a - // throw, whose unwind released everything down to depth 0). - const last = fn.body[fn.body.length - 1]?.kind; - const endedWithReturn = last === "return" || last === "throw" || last === "rethrow" || last === "runtimeFence"; - if (fn.returnType.kind === "void" && !endedWithReturn) { - emitter.releaseFrame(emitter.scopes[0]!); - } - emitter.scopes.pop(); - - emitter.indent--; - emitter.line(`}`); - // Keep generated runtime scaffolding out of the user's source file. - emitter.endSourceFunction(); - emitter.line(``); - } - -/** The statement bodies nested directly under a statement — the shared - * walk for returnCrossesFinally. */ - function childBodies(s: IrStmt): IrStmt[][] { - switch (s.kind) { - case "if": - return s.else_ ? [s.then, s.else_] : [s.then]; - case "while": - case "doWhile": - case "forOf": - case "block": - return [s.body]; - case "for": - return [s.body]; - case "switch": - return s.cases.map((c) => c.body); - case "tryCatch": - return [ - s.tryBody, - ...(s.catchBody ? [s.catchBody] : []), - ...(s.finallyBody ? [s.finallyBody] : []), - ]; - default: - return []; - } - } - -/** True when some `return` sits inside the tryBody/catchBody of a - * try-with-finally, at any nesting depth — exactly the returns the - * pending-return path routes, and so exactly when emitFunction must - * declare the sc_pret slot. */ - function returnCrossesFinally(stmts: IrStmt[]): boolean { - const hasReturn = (body: IrStmt[]): boolean => - body.some((s) => s.kind === "return" || childBodies(s).some(hasReturn)); - const walk = (body: IrStmt[]): boolean => - body.some((s) => { - if ( - s.kind === "tryCatch" && - s.finallyBody !== null && - (hasReturn(s.tryBody) || (s.catchBody !== null && hasReturn(s.catchBody))) - ) { - return true; - } - return childBodies(s).some(walk); - }); - return walk(stmts); - } - -/** Emits a block in its own lexical scope (refcounted locals released at - * 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). */ - export function emitBlock(emitter: CEmitter, stmts: IrStmt[], setup?: (scope: ScopeEntry[]) => void): void { - emitter.line(`{`); - emitter.indent++; - const scope: ScopeEntry[] = []; - emitter.scopes.push(scope); - setup?.(scope); - emitter.emitStmts(stmts); - const endedWithJump = endsWithJump(stmts); - emitter.scopes.pop(); - if (!endedWithJump) emitter.releaseFrame(scope); - emitter.indent--; - emitter.line(`}`); - } - -export function emitStmts(emitter: CEmitter, stmts: IrStmt[]): void { - for (const s of stmts) emitter.emitStmt(s); - } - -export function emitStmt(emitter: CEmitter, s: IrStmt): void { - const previous = emitter.sourceLoc; - emitter.sourceLoc = s.loc; - try { - emitStmtBody(emitter, s); - } finally { - emitter.sourceLoc = previous; - } - } - -function emitStmtBody(emitter: CEmitter, s: IrStmt): void { - emitter.frames.push([]); - switch (s.kind) { - case "varDecl": { - const local = emitter.currentLocals.get(s.localId)!; - const target = mangleLocal(s.localId); - if (s.init === null) { - // Declared, uninitialized (`let x: number;`). tsc's TS2454 rejects - // any read before assignment, so no runtime check is needed — but - // the declaration must still RESET the C local: inside a loop the - // previous iteration's scope exit released the old value and left - // a stale pointer, and the scope-exit release below runs whether - // or not an `assign` ever did (runtime releases are NULL-tolerant). - if (local.boxed) { - // The box must exist immediately: closures created before the - // first assignment capture it. (tsc's TS2454 can't see through - // closures — a call before the first assignment reads 0/false - // for scalars and traps for refcounted kinds, where JS has - // `undefined`.) - // A SCALAR TDZ box rides an ARR-kind box: the value lives in a - // one-element array cell, so the empty (NULL) slot stays the - // not-yet-initialized sentinel — a raw scalar slot has no spare - // bit pattern to spend on it. - const boxNew = - local.tdz && boxAccess(local.type) !== "ref" - ? "scr_box_new(SCR_BOX_ARR)" - : emitter.boxNewC(local.type); - emitter.line(`${target} = ${boxNew};${emitter.srcComment(s.loc)} /* let ${local.name}; */`); - emitter.scopes[emitter.scopes.length - 1]!.push({ name: target, type: local.type, boxed: true }); - } else if (isRefCounted(local.type)) { - emitter.line(`${target} = NULL;${emitter.srcComment(s.loc)} /* let ${local.name}; */`); - emitter.scopes[emitter.scopes.length - 1]!.push({ name: target, type: local.type }); - } - // Scalars need nothing: the C local exists from the prologue and - // no read happens before an assign writes it. - break; - } - if (local.boxed) { - // Box FIRST, then evaluate the initializer: a named function - // expression's closure captures this box during init evaluation. - emitter.line(`${target} = ${emitter.boxNewC(local.type)};${emitter.srcComment(s.loc)}`); - emitter.scopes[emitter.scopes.length - 1]!.push({ name: target, type: local.type, boxed: true }); - const v = emitter.emitExpr(s.init); - if (isRefCounted(v.type)) emitter.moveTemp(v); // the box takes ownership - emitter.line(`scr_box_set_${boxAccess(local.type)}(${target}, ${v.name});`); - break; - } - const v = emitter.emitExpr(s.init); - emitter.moveTemp(v); - emitter.line(`${target} = ${v.name};${emitter.srcComment(s.loc)}`); - if (isRefCounted(v.type)) { - emitter.scopes[emitter.scopes.length - 1]!.push({ name: target, type: v.type }); - } - break; - } - case "assign": { - const local = emitter.currentLocals.get(s.localId); - const concat = s.value; - const suffix = matchStringSelfConcat(s.localId, concat); - if (suffix && concat.kind === "strConcat") { - // Snapshot the old left value before evaluating the suffix. The - // snapshot is a normal frame-owned +1: the suffix may reassign the - // destination or throw, in which case unwinding must still release - // it while leaving the original binding intact. - const snapshot = emitter.emitExpr(concat.left); - const right = emitter.emitExpr(suffix); - const target = local ? mangleLocal(s.localId) : mangleGlobal(s.localId); - if (local?.boxed) { - // set_ref unlinks then releases the binding's CURRENT value. It - // may differ from snapshot when the suffix itself assigned target. - emitter.line(`scr_box_set_ref(${target}, NULL);`); - } else { - const old = `sc_t${emitter.tempCounter++}`; - emitter.line(`ScrStr *${old} = ${target};`); - emitter.line(`${target} = NULL;`); - emitter.releaseValue(old, snapshot.type); - } - const result = emitter.newTemp(s.value.type, `scr_str_concat(${snapshot.name}, ${right.name})`); - if (local?.boxed) { - emitter.moveTemp(result); // the box takes the concat result's +1 - emitter.line(`scr_box_set_ref(${target}, ${result.name});${emitter.srcComment(s.loc)}`); - } else { - emitter.moveTemp(result); // binding takes the concat result's +1 - emitter.line(`${target} = ${result.name};${emitter.srcComment(s.loc)}`); - } - break; - } - if (!local) { - // Module global: plain static storage, never boxed. Old-value - // release is NULL-tolerant (statics start NULL). - const g = emitter.globalsById.get(s.localId); - if (!g) throw new InternalCompilerError(`emitter bug: assign to unknown binding ${s.localId}`); - const target = mangleGlobal(g.id); - const v = emitter.emitExpr(s.value); - if (!s.initializes) checkGlobalTdz(emitter, g); - emitter.moveTemp(v); - if (isRefCounted(v.type)) emitter.releaseValue(target, v.type); - emitter.line(`${target} = ${v.name};${emitter.srcComment(s.loc)}`); - break; - } - const target = mangleLocal(s.localId); - const v = emitter.emitExpr(s.value); - if (local!.boxed) { - writeBox(emitter, local, v.name, s.initializes); - // A TDZ failure must still unwind the evaluated RHS. - if (isRefCounted(v.type)) emitter.moveTemp(v); - break; - } - emitter.moveTemp(v); - if (isRefCounted(v.type)) emitter.releaseValue(target, v.type); - emitter.line(`${target} = ${v.name};${emitter.srcComment(s.loc)}`); - break; - } - case "exprStmt": - emitter.emitExpr(s.expr); - break; - case "if": { - const cond = emitter.emitCondition(s.cond); - emitter.line(`if (${cond}) `); - emitter.mergeBrace(() => emitter.emitBlock(s.then)); - if (s.else_) { - emitter.line(`else `); - emitter.mergeBrace(() => emitter.emitBlock(s.else_!)); - } - break; - } - case "while": { - emitter.line(`for (;;) {${emitter.srcComment(s.loc)}`); - emitter.indent++; - const cond = emitter.emitCondition(s.cond); - emitter.line(`if (!(${cond})) break;`); - // C continue re-evaluates the condition at the top: exactly right - // for the unlabeled loop. A LABELED while allocates a continue - // label placed at the END of the body (falling off it re-enters - // the condition — the same point) so a labeled continue from a - // NESTED loop can goto it, and a lazy end label for labeled break. - const loop = emitter.loopTarget(null, s.labels); - emitter.jumpTargets.push(loop); - emitter.emitBlock(s.body); - emitter.jumpTargets.pop(); - if (loop.usedContinue && loop.continueLabel) emitter.line(`${loop.continueLabel}:;`); - emitter.indent--; - emitter.line(`}`); - if (loop.usedEnd) emitter.line(`${loop.endLabel}:;`); - break; - } - case "doWhile": { - // `for (;;) { body; cont: cond; if (!cond) break; }` — the body runs - // before the first condition check (at least once), and `continue` - // routes through the label so the condition still evaluates. - emitter.line(`for (;;) {${emitter.srcComment(s.loc)}`); - emitter.indent++; - const loop = emitter.loopTarget(`sc_cont_${emitter.labelCounter++}`, s.labels); - emitter.jumpTargets.push(loop); - emitter.emitBlock(s.body); - emitter.jumpTargets.pop(); - if (loop.usedContinue) emitter.line(`${loop.continueLabel}:;`); - const cond = emitter.emitCondition(s.cond); - emitter.line(`if (!(${cond})) break;`); - emitter.indent--; - emitter.line(`}`); - if (loop.usedEnd) emitter.line(`${loop.endLabel}:;`); - break; - } - case "switch": - emitter.emitSwitch(s); - break; - case "for": { - // Desugared in place; the init's scope wraps the whole loop, so - // break/continue must NOT release it (scopeDepth captured after). - const integerLoop = matchIntegerBytesForLoop(s, emitter.currentLocals); - emitter.line(`{${emitter.srcComment(s.loc)}`); - emitter.indent++; - emitter.scopes.push([]); - let integerShadow: string | null = null; - if (integerLoop) { - integerShadow = `sc_i${emitter.tempCounter++}`; - emitter.line(`uint64_t ${integerShadow} = 0; /* integer induction ${emitter.currentLocals.get(integerLoop.localId)!.name} */`); - emitter.integerLoopBindings.set(integerLoop.localId, integerShadow); - } else if (s.init) { - emitter.emitStmt(s.init); - } - emitter.line(`for (;;) {`); - emitter.indent++; - if (integerLoop && integerShadow) { - const receiver = emitStableReceiver(emitter, integerLoop.limitReceiver, []); - emitter.line(`if (!(${integerShadow} < ${receiver.name}->len)) break;`); - } else if (s.cond) { - const cond = emitter.emitCondition(s.cond); - emitter.line(`if (!(${cond})) break;`); - } - // C continue would skip the update; route it through a label instead. - const loop = emitter.loopTarget(`sc_cont_${emitter.labelCounter++}`, s.labels); - emitter.jumpTargets.push(loop); - emitter.emitBlock(s.body); - emitter.jumpTargets.pop(); - if (loop.usedContinue) emitter.line(`${loop.continueLabel}:;`); - // JS `for (let i ...)`: each iteration gets a FRESH binding holding a - // copy of the previous one, and the update mutates the fresh binding - // — that's why closures made in iteration k keep seeing iteration - // k's value. Only observable (and only emitted) when captured. - if (s.init?.kind === "varDecl") { - const initLocal = emitter.currentLocals.get(s.init.localId); - if (initLocal?.boxed) { - const box = mangleLocal(initLocal.id); - const fresh = `sc_t${emitter.tempCounter++}`; - const acc = boxAccess(initLocal.type); - emitter.line(`ScrBox *${fresh} = ${emitter.boxNewC(initLocal.type)}; /* per-iteration ${initLocal.name} */`); - emitter.line(`scr_box_set_${acc}(${fresh}, scr_box_get_${acc}(${box}));`); - emitter.line(`scr_box_release(${box});`); - emitter.line(`${box} = ${fresh};`); - // The wrapper scope's entry releases whatever `box` points to at - // loop exit — which is now the freshest binding. Nothing to fix. - } - } - if (integerLoop && integerShadow) emitter.line(`${integerShadow}++;`); - else if (s.update) emitter.emitStmt(s.update); - emitter.indent--; - emitter.line(`}`); - // A labeled break lands exactly where C break does: BEFORE the - // init scope's release (the goto path must run the same releases - // as the fall-through path). - if (loop.usedEnd) emitter.line(`${loop.endLabel}:;`); - emitter.releaseFrame(emitter.scopes.pop()!); - if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); - emitter.indent--; - emitter.line(`}`); - break; - } - case "arraySet": { - // Evaluation order matches JS: array, index, then value. Ownership - // of a refcounted value moves into the array (the runtime releases - // the replaced element itself). - const arr = emitter.emitExpr(s.arr); - const idx = emitter.emitExpr(s.index); - const v = emitter.emitExpr(s.value); - if (s.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arraySet on non-array"); - const acc = elemAccess(s.arr.type.elem); - if (acc === "ref") emitter.moveTemp(v); - emitter.line(`scr_arr_set_${acc}(${arr.name}, ${idx.name}, ${v.name});${emitter.srcComment(s.loc)}`); - break; - } - case "arraySetLength": { - const arr = emitter.emitExpr(s.arr); - const length = emitter.emitExpr(s.length); - if (s.arr.type.kind !== "array") throw new InternalCompilerError("emitter bug: arraySetLength on non-array"); - emitter.line(`scr_arr_set_len(${arr.name}, ${length.name});${emitter.srcComment(s.loc)}`); - emitter.emitPendingCheck(); - break; - } - case "arraySetUndefined": - case "arrayDelete": { - const arr = emitter.emitExpr(s.arr); - const idx = emitter.emitExpr(s.index); - if (s.arr.type.kind !== "array") throw new InternalCompilerError(`emitter bug: ${s.kind} on non-array`); - emitter.line(`${s.kind === "arraySetUndefined" ? "scr_arr_set_undefined" : "scr_arr_delete"}(${arr.name}, ${idx.name});${emitter.srcComment(s.loc)}`); - break; - } - case "bytesSet": { - // Typed-array element write: same evaluation order as arraySet; - // the value is a scalar (the inline kind-specific accessor coerces - // JS-exactly), so no ownership moves. Any invalid index traps — no - // append. The IR carries the element kind; do not rediscover it in - // the generic runtime accessor on every loop iteration. - const arr = emitStableReceiver(emitter, s.arr, [s.index, s.value]); - const integerIndex = emitter.integerLoopIndex(s.index); - const idx = integerIndex === null ? emitter.emitExpr(s.index).name : integerIndex; - const v = emitter.emitExpr(s.value); - if (s.arr.type.kind !== "bytes") throw new InternalCompilerError("emitter bug: bytesSet on non-bytes"); - emitter.line( - `${emitter.bytesElementHelper("set", s.arr.type.elem, integerIndex !== null)}(${arr.name}, ${idx}, ${v.name});${emitter.srcComment(s.loc)}`, - ); - break; - } - case "fieldSet": - case "recordSet": { - // Evaluation order: obj, then value. New value moved in; the old - // value is released AFTER the field is overwritten (releases are - // NULL-tolerant — fields start NULL from the zeroed allocation). - // Unlink-then-release is load-bearing: a release can trigger a - // cycle collection, which must never see a heap edge whose count - // was already given up (scr_cycle.c). Classes and records share - // the struct layout, so one emission. - const obj = emitter.emitExpr(s.obj); - const v = emitter.emitExpr(s.value); - // Runtime error classes use ScrError's own member names. - const member = - s.kind === "fieldSet" && RUNTIME_ERROR_CLASSES.has(s.className) - ? s.field - : mangleField(s.field); - const field = `${obj.name}->${member}`; - if (isRefCounted(v.type)) { - emitter.moveTemp(v); - const old = `sc_t${emitter.tempCounter++}`; - emitter.line(`${cDecl(v.type, old)} = ${field};`); - emitter.line(`${field} = ${v.name};${emitter.srcComment(s.loc)}`); - emitter.releaseValue(old, v.type); - } else { - emitter.line(`${field} = ${v.name};${emitter.srcComment(s.loc)}`); - } - break; - } - case "recordKeyDelete": { - // `delete obj[k]` on a pure index-signature shape: a Map delete on - // the overflow (key and value released; absent keys no-op). - const obj = emitter.emitExpr(s.obj); - const key = emitter.emitExpr(s.key); - emitter.line(`scr_map_delete_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name});${emitter.srcComment(s.loc)}`); - break; - } - case "recordKeySet": { - // Dynamic-keyed record write through the per-shape helper (declared - // keys write through with validation, undeclared keys land in the - // overflow map). Evaluation order: obj, key, value. The helper OWNS - // the value (+1 moves in). MAY THROW when a dyn value must validate - // against a declared field's type — the pending check runs then. - const obj = emitter.emitExpr(s.obj); - const key = emitter.emitExpr(s.key); - const v = emitter.emitExpr(s.value); - if (isRefCounted(v.type)) emitter.moveTemp(v); - // A LITERAL key naming no declared field is a plain overflow map - // insert — no helper, no validation, no throw. - if (s.overflowOnly) { - const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref"; - emitter.line( - `scr_map_set_str_${acc}(${obj.name}->${OVERFLOW_MEMBER}, ${key.name}, ${v.name});${emitter.srcComment(s.loc)}`, - ); - break; - } - const helper = emitter.recordKeySetHelper(s.shapeId); - emitter.line(`${helper}(${obj.name}, ${key.name}, ${v.name});${emitter.srcComment(s.loc)}`); - const shape = emitter.recordsById.get(s.shapeId); - // MAY THROW: a dyn value validating against a declared field, or a - // signature-free shape's key MISS (scr_record_key_miss). - if (shape && (!shape.indexValue || (shape.indexValue.kind === "dyn" && shape.fields.length > 0))) { - emitter.emitPendingCheck(); - } - break; - } - case "forOf": { - // Ascending index loop; the length is re-read every iteration - // (JS-exact — pushes inside the body extend the iteration). The - // iterable temp lives in this statement's frame, so it is released - // when the whole loop ends (and by `return`'s frame sweep). A real - // C for-loop makes plain `continue` correct: the update still runs. - if (s.iterable.type.kind !== "array") throw new InternalCompilerError("emitter bug: forOf over non-array"); - const elem = s.iterable.type.elem; - const arr = emitter.emitExpr(s.iterable); - const idx = `sc_t${emitter.tempCounter++}`; - emitter.line( - `for (double ${idx} = 0; ${idx} < scr_arr_len(${arr.name}); ${idx} += 1) {${emitter.srcComment(s.loc)}`, - ); - emitter.indent++; - // A real C for-loop makes plain `continue` correct (the update - // still runs); a LABELED forOf allocates a continue label placed at - // the END of the iteration (after the per-iteration scope release — - // the goto path released it itself) so nested loops can goto it. - const loop = emitter.loopTarget(null, s.labels); - emitter.jumpTargets.push(loop); - // The loop variable is a fresh const per iteration: its scope opens - // here, holds the (for ref elements: owned +1) current element, and - // releases it at the end of each iteration. - emitter.scopes.push([]); - const local = mangleLocal(s.localId); - const localInfo = emitter.currentLocals.get(s.localId); - if (localInfo?.boxed) { - // Captured loop variable: a fresh box per iteration, matching the - // fresh const binding (closures made in iteration k keep seeing - // iteration k's element). The box takes ownership of a ref - // element's +1 and is released with the iteration's scope. - emitter.line(`${local} = ${emitter.boxNewC(elem)}; /* per-iteration ${localInfo.name} */`); - emitter.line(`scr_box_set_${boxAccess(elem)}(${local}, scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx}));`); - emitter.scopes[emitter.scopes.length - 1]!.push({ name: local, type: elem, boxed: true }); - } else { - emitter.line(`${local} = scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx});`); - if (isRefCounted(elem)) emitter.scopes[emitter.scopes.length - 1]!.push({ name: local, type: elem }); - } - emitter.emitStmts(s.body); - const endedWithJump = endsWithJump(s.body); - const scope = emitter.scopes.pop()!; - if (!endedWithJump) emitter.releaseFrame(scope); - emitter.jumpTargets.pop(); - if (loop.usedContinue && loop.continueLabel) emitter.line(`${loop.continueLabel}:;`); - emitter.indent--; - emitter.line(`}`); - if (loop.usedEnd) emitter.line(`${loop.endLabel}:;`); - break; - } - case "block": { - if (s.labels === undefined) { - emitter.emitBlock(s.body); - break; - } - // A labeled block: `break lbl` inside jumps to the end label - // (releasing the block's scope itself); nothing else can target it. - const target = { - kind: "block" as const, - endLabel: `sc_end_${emitter.labelCounter++}`, - usedEnd: false, - labels: s.labels, - scopeDepth: emitter.scopes.length, - frameDepth: emitter.frames.length, - finallyDepth: emitter.finallyStack.length, - }; - emitter.jumpTargets.push(target); - emitter.emitBlock(s.body); - emitter.jumpTargets.pop(); - if (target.usedEnd) emitter.line(`${target.endLabel}:;`); - break; - } - case "break": { - // Unlabeled: binds to the innermost loop OR switch (labeled block - // targets are skipped). Labeled: binds to the entry carrying the - // label. Release every scope entered since that target started - // (the body scope and anything nested); their natural end-of-block - // releases are on the fall-through path this jump bypasses. Same - // for pending frame temps of statements the jump exits (a nested - // switch's discriminant) — but NOT the target's own frame: a - // loop's releases after the loop and a switch's after its end - // label are still on this jump's path. - let target: (typeof emitter.jumpTargets)[number] | undefined; - for (let i = emitter.jumpTargets.length - 1; i >= 0; i--) { - const t = emitter.jumpTargets[i]!; - if (s.label !== undefined ? t.labels?.includes(s.label) : t.kind !== "block") { - target = t; - break; - } - } - if (!target) throw new InternalCompilerError("emitter bug: break target not found"); - emitter.emitFinallysForJump(target.finallyDepth, target.frameDepth, target.scopeDepth); - if (target.kind !== "loop" || s.label !== undefined) { - // Switches are emitted as goto chains and blocks aren't C loops - // at all, so a C `break` cannot target either; and a LABELED - // break may target an outer loop a C break would never reach. - // All three jump to the target's end label (labeled loops always - // allocate one — loopTarget). - target.usedEnd = true; - emitter.line(`goto ${target.endLabel!};${emitter.srcComment(s.loc)}`); - } else { - emitter.line(`break;${emitter.srcComment(s.loc)}`); - } - break; - } - case "continue": { - // Unlabeled: binds to the innermost LOOP, skipping any switches and - // labeled blocks in between (their scopes are still released — the - // jump exits them). Labeled: binds to the loop carrying the label. - let loop: ((typeof emitter.jumpTargets)[number] & { kind: "loop" }) | undefined; - for (let i = emitter.jumpTargets.length - 1; i >= 0; i--) { - const t = emitter.jumpTargets[i]!; - if (t.kind === "loop" && (s.label === undefined || t.labels?.includes(s.label))) { - loop = t; - break; - } - } - if (!loop) throw new InternalCompilerError("emitter bug: continue target not found"); - emitter.emitFinallysForJump(loop.finallyDepth, loop.frameDepth, loop.scopeDepth); - if (loop.continueLabel) { - // Labeled loops always allocate one (a labeled continue may - // target an outer loop a C continue could never reach); for/ - // do-while allocate one for their update/condition point. - loop.usedContinue = true; - emitter.line(`goto ${loop.continueLabel};${emitter.srcComment(s.loc)}`); - } else { - emitter.line(`continue;${emitter.srcComment(s.loc)}`); - } - break; - } - case "return": { - // A return inside the pending-return copy of a finally REPLACES the - // old return. Keep the old slot protected while the new expression - // evaluates (a throw there still discards it), then release the old - // value and remove the slot from this return's own unwind path. - let pendingEntry: { index: number; entry: (typeof emitter.scopes)[number][number] } | null = null; - let value: ReturnType | null = null; - if (s.value) { - const emitted = emitter.emitExpr(s.value); - // A void expression still runs before finally, but has no C value - // to park in the pending-return slot (or return from the function). - if (s.value.type.kind !== "void") value = emitted; - } - const pendingIndex = emitter.pendingReturnScopeIndex; - if (pendingIndex !== null && isRefCounted(emitter.currentReturnType)) { - const scope = emitter.scopes[pendingIndex]!; - const entryIndex = scope.findIndex((entry) => entry.name === "sc_pret"); - if (entryIndex < 0) throw new InternalCompilerError("emitter bug: pending return scope has no sc_pret"); - pendingEntry = { index: pendingIndex, entry: scope.splice(entryIndex, 1)[0]! }; - emitter.releaseValue("sc_pret", emitter.currentReturnType); - } - const fin = emitter.finallyStack[emitter.finallyStack.length - 1]; - if (fin) { - // Crossing ≥1 finally: the value is computed and snapshotted - // FIRST (a finally mutating the returned local cannot change - // it — Node's semantics), then everything down to the innermost - // region releases and control runs that region's pending-return - // finally copy; its tail dispatches further out or returns. - if (value) { - emitter.moveTemp(value); // ownership parks in the slot until the dispatch returns it - emitter.line(`sc_pret = ${value.name};${emitter.srcComment(s.loc)}`); - } - fin.used = true; - emitter.releaseForJump(fin.frameDepth, fin.scopeDepth); - emitter.line(`goto ${fin.label};${s.value ? "" : emitter.srcComment(s.loc)}`); - } else if (value) { - emitter.moveTemp(value); - // Everything down to function depth releases; the moved result is - // exempt (already struck from its frame). - emitter.releaseForJump(0, 0); - emitter.line(`return ${value.name};${emitter.srcComment(s.loc)}`); - } else { - emitter.releaseForJump(0, 0); - emitter.line(`return;${emitter.srcComment(s.loc)}`); - } - if (pendingEntry !== null) { - emitter.scopes[pendingEntry.index]!.push(pendingEntry.entry); - } - break; - } - case "throw": { - // Evaluate, move ownership into the runtime's exception cell, then - // unwind unconditionally (the innermost try handler, or out of the - // function) — the same release path as return/break/continue. - const v = emitter.emitExpr(s.value); - const t = s.value.type; - if (isRefCounted(t)) emitter.moveTemp(v); // the cell takes ownership - if (t.kind === "date") { - throw new InternalCompilerError("emitter bug: Date throw reached backend"); - } else if (t.kind === "f64") { - emitter.line(`scr_throw_f64(${v.name});${emitter.srcComment(s.loc)}`); - } else if (t.kind === "bool") { - emitter.line(`scr_throw_bool(${v.name});${emitter.srcComment(s.loc)}`); - } else if (t.kind === "string") { - emitter.line(`scr_throw_str(${v.name});${emitter.srcComment(s.loc)}`); - } else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) { - // Hierarchy instances carry a vtable word: the OBJ kind keeps the - // dynamic class inspectable (catch-binding instanceof, and the - // uncaught printer's "name: message" for Error instances). - const rc = vAdapters(t); - emitter.line(`scr_throw_obj(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(s.loc)}`); - } else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") { - const rc = vAdapters(t); - emitter.line(`scr_throw_primitive_ref(${v.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(s.loc)}`); - } else if (t.kind === "dyn") { - emitter.line(`scr_dyn_throw(${v.name});${emitter.srcComment(s.loc)}`); - } else if (t.kind === "jsval") { - const rc = vAdapters(t); - const test = "scr_jsval_is_object"; - emitter.line(`scr_throw_ref_classified(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${v.name}));${emitter.srcComment(s.loc)}`); - } else { - const rc = vAdapters(t); - emitter.line(`scr_throw_ref(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(s.loc)}`); - } - emitter.emitUnwind(); - break; - } - case "runtimeFence": { - // The deferred JS compile fence: throw a catchable Error naming - // the construct (message) with the SC code stamped on `code`, - // then unwind exactly like `throw`. - const bytes = Buffer.from(s.message, "utf8"); - emitter.line( - `scr_throw_error_msg_code(SCR_ERR_ERROR, ${cStringLiteral(bytes)}, ${bytes.length}, "${s.code}");${emitter.srcComment(s.loc)}`, - ); - emitter.emitUnwind(); - break; - } - case "rethrow": - // Re-raise the saved snapshot (payload retained — the binding local - // releases with its scope) and unwind like `throw`. - emitter.line(`scr_rethrow(${mangleLocal(s.localId)});${emitter.srcComment(s.loc)}`); - emitter.emitUnwind(); - break; - case "tryCatch": - emitter.emitTryCatch(s); - break; - default: { - const _exhaustive: never = s; - void _exhaustive; - } - } - const frame = emitter.frames.pop()!; - // return/throw already released their frames on the jump path; emitting - // the fall-through releases after them would be dead double-release code. - if (s.kind !== "return" && s.kind !== "throw" && s.kind !== "rethrow" && s.kind !== "runtimeFence") emitter.releaseFrame(frame); - } - -/** try/catch/finally via pending-flag unwinding. Entering a try emits NO - * code: the try context is compile-time state (tryStack) that redirects - * unwinds inside the region to a label here instead of out of the - * function. Shape: - * - * { try body } unwinds inside release frames/scopes down to - * this statement's depths, then goto the handler - * goto after; (normal completion skips the handler) - * sc_catch_N:; (emitted only when some unwind targets it) - * scr_exc_clear(); catch TAKES the exception (payload discarded — - * { catch body } the supported catch form is bindingless) - * after/sc_fin_N:; - * { finally body } normal path - * goto sc_tryend_N; - * sc_finexc_N:; exception path: pending flag still set - * { finally body } (emitted twice — fresh temps/labels each time; - * duplication is safe, and the pending flag is - * sc_tryend_N:; the dispatch) - * - * Without a catch, unwinds in the try body target sc_finexc_N directly. - * A catch body's own exceptions target sc_finexc_N (when a finally - * exists) or the ENCLOSING context — never its own handler. After the - * exception-path finally, propagation continues (emitUnwind: enclosing - * handler or dummy return). A `return` inside the try/catch body rides a - * THIRD finally copy (sc_finret_N — the pending-return path: value - * snapshotted into sc_pret at the return site, dispatch outward after - * the copy runs). Break/continue inline the crossed finally bodies via - * emitFinallysForJump before branching to their resolved targets; jumps - * out of plain try/catch need nothing beyond release-on-jump. */ - export function emitTryCatch(emitter: CEmitter, s: IrStmt & { kind: "tryCatch" }): void { - const id = emitter.labelCounter++; - const hasCatch = s.catchBody !== null; - const hasFinally = s.finallyBody !== null; - const catchLabel = `sc_catch_${id}`; - const finExcLabel = `sc_finexc_${id}`; - const endLabel = `sc_tryend_${id}`; - // Where the try body's normal completion continues. - const afterTryLabel = hasFinally ? `sc_fin_${id}` : endLabel; - let afterTryLabelUsed = false; - - const handler = { - label: hasCatch ? catchLabel : finExcLabel, - used: false, - frameDepth: emitter.frames.length, - scopeDepth: emitter.scopes.length, - }; - // The pending-return region: returns inside tryBody/catchBody snapshot - // their value and jump here-ish (the pending-return finally copy below) - // instead of returning directly. Same depths as the unwind handler. - const retEntry = hasFinally - ? { - label: `sc_finret_${id}`, - used: false, - frameDepth: emitter.frames.length, - scopeDepth: emitter.scopes.length, - tryDepth: emitter.tryStack.length, - body: s.finallyBody!, - } - : null; - emitter.line(`/* try */${emitter.srcComment(s.loc)}`); - if (retEntry) emitter.finallyStack.push(retEntry); - emitter.tryStack.push(handler); - emitter.emitBlock(s.tryBody); - emitter.tryStack.pop(); - - // Exceptions raised in the CATCH body unwind to the exception-path - // finally (pending stays set through it) when one exists. - const excHandler = { - label: finExcLabel, - used: !hasCatch && handler.used, - frameDepth: emitter.frames.length, - scopeDepth: emitter.scopes.length, - }; - - if (hasCatch && handler.used) { - if (!endsWithJump(s.tryBody)) { - emitter.line(`goto ${afterTryLabel};`); - afterTryLabelUsed = true; - } - if (hasFinally) emitter.tryStack.push(excHandler); - // Generator bodies: a pending GENRET sentinel (.return(v) injected at - // a yield) is a RETURN completion, not a throw — catch must not take - // it. Re-unwind past this handler (finally still runs — the unwind - // targets the exception-path finally or the enclosing context; the - // runtime depths here equal the handler's, so no double release). - const genretPrologue = (): void => { - if (emitter.currentGenerator === null) return; - emitter.line(`if (scr_exc_genret_pending()) { /* .return(): not catchable */`); - emitter.indent++; - emitter.emitUnwind(); - emitter.indent--; - emitter.line(`}`); - }; - if (s.catchLocalId !== null) { - // catch (e): the exception MOVES into the binding's snapshot box, - // owned by the catch body's scope (released on every exit — normal - // fall-through, jumps out, and unwinds from the body). - const binding = mangleLocal(s.catchLocalId); - emitter.line(`${catchLabel}:; /* catch (${emitter.currentLocals.get(s.catchLocalId)?.name ?? "e"}) — takes the exception */`); - genretPrologue(); - emitter.emitBlock(s.catchBody!, (scope) => { - emitter.line(`${binding} = scr_exc_take();`); - scope.push({ name: binding, type: CAUGHT }); - }); - } else { - emitter.line(`${catchLabel}:; /* catch — takes the exception */`); - genretPrologue(); - emitter.line(`scr_exc_clear();`); - emitter.emitBlock(s.catchBody!); - } - if (hasFinally) emitter.tryStack.pop(); - // Normal completion of the catch falls through to afterTryLabel. - } - if (retEntry) emitter.finallyStack.pop(); - - if (hasFinally) { - if (afterTryLabelUsed) emitter.line(`${afterTryLabel}:;`); - emitter.line(`/* finally (normal path) */`); - emitter.emitBlock(s.finallyBody!); - const needEnd = excHandler.used || retEntry!.used; - if (needEnd) emitter.line(`goto ${endLabel};`); - if (excHandler.used) { - // The pending exception is STASHED across the finally body (a - // ScrCaught snapshot, re-raised after) so the body runs with a - // CLEAN cell: its own may-throw calls' pending checks answer for - // themselves — not for the in-flight exception — and a generator - // suspending here (a yield inside a finally on the .return()/ - // .throw() unwind) resumes into the REST of the finally, exactly - // Node. A throw inside the body REPLACES the stash (it unwinds - // past the release through the synthetic scope entry below — - // JS's replace semantics); normal completion re-raises the stash - // and keeps propagating. - const stash = `sc_fexc_${id}`; - emitter.line(`${finExcLabel}:; /* finally (exception path — stashed) */`); - emitter.line(`ScrCaught *${stash} = scr_exc_take();`); - emitter.scopes.push([{ name: stash, type: CAUGHT }]); - const suppressHandler = s.suppressFinallyErrors - ? { - label: `sc_fsup_${id}`, - used: false, - frameDepth: emitter.frames.length, - scopeDepth: emitter.scopes.length, - } - : null; - if (suppressHandler) emitter.tryStack.push(suppressHandler); - emitter.emitBlock(s.finallyBody!); - if (suppressHandler) emitter.tryStack.pop(); - emitter.scopes.pop(); // normal completion keeps the stash for the re-raise - emitter.line(`scr_rethrow(${stash});`); - emitter.line(`scr_caught_release(${stash});`); - emitter.emitUnwind(); - if (suppressHandler?.used) { - emitter.line(`${suppressHandler.label}:; /* disposal error suppresses body error */`); - emitter.line(`scr_exc_suppress(${stash}); /* consumes stash */`); - emitter.emitUnwind(); - } - } - if (retEntry!.used) { - // Pending-return path: a return in the try/catch body parked its - // value in sc_pret and jumped here after releasing down to this - // region. The finally body runs (third copy — fresh temps/labels, - // like the exception copy), then the dispatch continues outward: - // the next enclosing finally region of this function, or the - // actual return. A THROW inside this copy replaces the pending - // return (JS): the slot's owned value rides a synthetic scope - // entry so the unwind releases it. - emitter.line(`${retEntry!.label}:; /* finally (pending-return path) */`); - const retT = emitter.currentReturnType; - const own = isRefCounted(retT); - const previousPendingScope = emitter.pendingReturnScopeIndex; - if (own) { - emitter.scopes.push([{ name: "sc_pret", type: retT }]); - emitter.pendingReturnScopeIndex = emitter.scopes.length - 1; - } - emitter.emitBlock(s.finallyBody!); - if (own) emitter.scopes.pop(); - emitter.pendingReturnScopeIndex = previousPendingScope; - const outer = emitter.finallyStack[emitter.finallyStack.length - 1]; - if (outer) { - outer.used = true; - emitter.releaseForJump(outer.frameDepth, outer.scopeDepth); - emitter.line(`goto ${outer.label};`); - } else { - emitter.releaseForJump(0, 0); - emitter.line(retT.kind === "void" ? `return;` : `return sc_pret;`); - } - } - if (needEnd) emitter.line(`${endLabel}:;`); - } else if (afterTryLabelUsed) { - emitter.line(`${endLabel}:;`); - } - } - -/** JS-exact switch as a goto chain — C `switch` cannot express lazily - * evaluated, arbitrary-expression case tests. Shape: - * - * disc temp - * NULL-reset of refcounted case-body locals (see below) - * per test, in source order: eval test; if (== disc) goto case_i; - * goto default (or end when there is none) - * case_0:; body_0 ─┐ bodies in source order fall through - * case_1:; body_1 ─┘ naturally, JS-exact - * scope releases (natural fall-off path) - * end:; (break jumps here, after releasing scopes itself) - * - * All case bodies share ONE scope (JS: one lexical scope per switch body). - * Because dispatch can jump PAST a varDecl into a later case, a refcounted - * local of a skipped body is never written — and inside an enclosing loop - * it still holds the pointer a previous iteration's scope exit already - * released. The declarations are therefore NULL-reset up front; the - * scope-exit releases rely on the runtime's NULL-tolerant release calls. */ - export function emitSwitch(emitter: CEmitter, s: IrStmt & { kind: "switch" }): void { - if (s.disc.type.kind !== "f64" && s.disc.type.kind !== "string" && s.disc.type.kind !== "bool") { - throw new InternalCompilerError(`emitter bug: switch on ${s.disc.type.kind}`); - } - const id = emitter.labelCounter++; - const endLabel = `sc_swend_${id}`; - const caseLabel = (i: number) => `sc_swcase_${id}_${i}`; - - // The disc temp lives in the whole statement's frame: for a string - // discriminant it stays alive across every test and body, released when - // the switch statement ends (break lands before that release; return - // sweeps frames itself). - const disc = emitter.emitExpr(s.disc); - - // NULL-reset refcounted/boxed locals declared at the top level of case - // bodies (nested blocks manage their own scopes on normal control flow). - for (const c of s.cases) { - for (const stmt of c.body) { - if (stmt.kind !== "varDecl") continue; - const local = emitter.currentLocals.get(stmt.localId)!; - if (local.boxed || isRefCounted(local.type)) { - emitter.line(`${mangleLocal(local.id)} = NULL; /* case-scoped ${local.name} */`); - } - } - } - - // Dispatch: lazy source-order test evaluation (a test after the match - // never runs). Each test's temps release right after its comparison. - let defaultIdx = -1; - s.cases.forEach((c, i) => { - if (c.test === null) { - defaultIdx = i; - return; - } - emitter.frames.push([]); - const t = emitter.emitExpr(c.test); - const cmp = - c.test.type.kind === "string" - ? `scr_str_eq(${disc.name}, ${t.name})` - : `${disc.name} == ${t.name}`; - const hit = emitter.newTemp(BOOL, cmp); - emitter.releaseFrame(emitter.frames.pop()!); - emitter.line(`if (${hit.name}) goto ${caseLabel(i)};`); - }); - emitter.line(`goto ${defaultIdx >= 0 ? caseLabel(defaultIdx) : endLabel};`); - - // Bodies in source order: entering one falls through the rest (JS-exact) - // until a break jumps to the end label. - const target = { - labels: s.labels, - kind: "switch" as const, - endLabel, - usedEnd: defaultIdx < 0, - scopeDepth: emitter.scopes.length, - frameDepth: emitter.frames.length, - finallyDepth: emitter.finallyStack.length, - }; - emitter.jumpTargets.push(target); - emitter.scopes.push([]); - s.cases.forEach((c, i) => { - emitter.line(`${caseLabel(i)}:;`); - emitter.indent++; - emitter.emitStmts(c.body); - emitter.indent--; - }); - emitter.jumpTargets.pop(); - const scope = emitter.scopes.pop()!; - // Natural fall-off of the last body releases the shared scope here; a - // jump (break/return/continue/throw) already released it before jumping. - const lastBody = s.cases[s.cases.length - 1]?.body; - if (!lastBody || !endsWithJump(lastBody)) emitter.releaseFrame(scope); - if (target.usedEnd) emitter.line(`${endLabel}:;`); - } - -/** Emits `if (cond) ` followed by a block on the same line for readability. */ - export function mergeBrace(emitter: CEmitter, emitBlockFn: () => void): void { - const head = emitter.lines.pop()!; - let before = emitter.lines.length; - emitBlockFn(); - while (emitter.lines[before]?.startsWith("#line ")) before++; - emitter.lines[before] = head + emitter.lines[before]!.trimStart(); - } - -/** Evaluates `expr` in its own statement frame inside an already-open - * branch and moves the result into the C lvalue `target`: the chosen - * value's ownership transfers, every other temp the evaluation allocated - * releases inside the branch. The shared core of the lazily-branched - * expressions (`logical`, `ternary`); the caller owns the surrounding - * braces/indentation and registers `target` in ITS frame. */ - export function emitBranchInto(emitter: CEmitter, target: string, expr: IrExpr): void { - emitter.frames.push([]); - const v = emitter.emitExpr(expr); - emitter.moveTemp(v); - emitter.line(`${target} = ${v.name};`); - emitter.releaseFrame(emitter.frames.pop()!); - } - -/** Evaluates a condition; its string temps are released before the branch, - * which is safe because the result is a scalar bool. */ - export function emitCondition(emitter: CEmitter, cond: IrExpr): string { - const t = emitter.emitExpr(cond); - const frame = emitter.currentFrame(); - emitter.releaseFrame(frame); - frame.length = 0; - return t.name; - } diff --git a/packages/compiler/src/backend/c/types.test.ts b/packages/compiler/src/backend/c/types.test.ts deleted file mode 100644 index 887eff78b..000000000 --- a/packages/compiler/src/backend/c/types.test.ts +++ /dev/null @@ -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", - }); - }); -}); diff --git a/packages/compiler/src/backend/c/types.ts b/packages/compiler/src/backend/c/types.ts deleted file mode 100644 index 2e829b362..000000000 --- a/packages/compiler/src/backend/c/types.ts +++ /dev/null @@ -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 -}>; -type RejectedArrayPointerType = Extract -}>; - -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; - 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; - void _exhaustive; - throw new InternalCompilerError("unreachable"); - } - } -} - -/** The runtime's element-kind tag for a bytes (typed array) type. */ -const BYTES_ELEM_KIND_C: Record = { - 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 = { - 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 = { - 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 = { - 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 = { - 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"; -} - -/** ` ` 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}`; -} diff --git a/packages/compiler/src/backend/c/walkers.ts b/packages/compiler/src/backend/c/walkers.ts deleted file mode 100644 index 3173fc0c7..000000000 --- a/packages/compiler/src/backend/c/walkers.ts +++ /dev/null @@ -1,2200 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; -/* Structure-walking helper EMITTERS: interned per-type C helper functions - * for JSON serialization, dyn matching/validation (dynCheck), dyn value - * descriptions, and whole-union truthiness/equality. Each helper is emitted - * once per type shape and cached in the emitter's registries (jsonWriters, - * dynMatchers, dynBuilders, unionTruthyFns, unionEqFns, walkerProtos/Defs) — - * interning ORDER is part of the emitted C, so the registries stay on - * CEmitter and these functions only consult them through it. */ -import type { CEmitter } from "./c-emitter.js"; -import { DYN_HANDLE_KINDS, type IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; -import { dynDesc, undefinedArmTag, unionWideningTags } from "../../ir/analysis.js"; -import { bytesElemKindC, cCommentText, cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./types.js"; -import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js"; -import { jsonObjectKeyLabel } from "../json-literal.js"; -import { OVERFLOW_MEMBER } from "./shapes.js"; - -/** Check the literal field before the structural matcher traverses a - * variant. Besides choosing the right layout, this avoids recursively - * probing every overlapping shape in a large AST union. */ -function unionArmMatch(emitter: CEmitter, def: IrUnionDef, tag: number): string { - const match = `${emitter.dynMatchHelper(def.arms[tag]!)}(d)`; - const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag); - if (!guard || !def.discriminant) return match; - const field = `(ScrStr *)&${emitter.internLiteral(def.discriminant.field)}`; - const values = guard.values.map((value) => { - if (typeof value === "string") { - return `scr_dyn_field_eq_str(d, ${field}, (ScrStr *)&${emitter.internLiteral(value)})`; - } - if (typeof value === "boolean") return `scr_dyn_field_eq_bool(d, ${field}, ${value})`; - return `scr_dyn_field_eq_num(d, ${field}, ${Object.is(value, -0) ? "-0.0" : String(value)})`; - }); - return `((${values.join(" || ")}) && ${match})`; -} - -/** Retag a borrowed union into a superset without copying its payload. - * Both declared fields and owned overflow reads use the same helper; - * owned callers release their input after the new box retains the payload. */ -export function unionWidenHelper(emitter: CEmitter, fromId: string, toId: string): string { - const key = `${fromId}:${toId}`; - const existing = emitter.unionWidenFns.get(key); - if (existing) return existing; - const source = emitter.unionsById.get(fromId); - const target = emitter.unionsById.get(toId); - const tags = source && target ? unionWideningTags(source.arms, target.arms) : null; - if (!source || !tags) throw new InternalCompilerError(`emitter bug: cannot widen ${fromId} into ${toId}`); - const name = `sc_uw_${emitter.unionWidenFns.size}`; - emitter.unionWidenFns.set(key, name); - emitter.walkerProtos.push(`static ScrUnion *${name}(ScrUnion *v);`); - const lines = [`static ScrUnion *${name}(ScrUnion *v) {`, " switch (v->tag) {"]; - source.arms.forEach((arm, index) => { - const tag = tags[index]!; - let result: string; - if (arm.kind === "nullT" || arm.kind === "undefinedT") { - result = emitter.unitInstanceRef(toId, tag); - } else if (arm.kind === "f64" || arm.kind === "bool") { - result = `scr_union_new_${arm.kind}(${tag}, scr_union_get_${arm.kind}(v))`; - } else { - const adapters = vAdapters(arm); - const payload = retainCallC(arm, `(${cType(arm).trim()})scr_union_peek(v)`); - result = `scr_union_new_ref(${tag}, ${payload}, &${adapters.retain}, &${adapters.release}, ${emitter.traceArgC(arm)})`; - } - lines.push(` case ${index}: return ${result};`); - }); - lines.push(' default: scr_trap("scriptc: internal error: invalid union tag\\n");', " }", "}"); - emitter.walkerDefs.push(lines.join("\n")); - return name; -} - -/** The per-union ToBoolean helper (interned per unionId): switch on the - * runtime tag — unit arms false, f64 arms 0/NaN-falsy, string arms - * empty-falsy, bool arms by payload, ref arms always true (JS objects), - * jsval arms answered by the engine. Borrows the operand. */ - export function unionTruthyHelper(emitter: CEmitter, unionId: string): string { - const existing = emitter.unionTruthyFns.get(unionId); - if (existing) return existing; - const def = emitter.unionsById.get(unionId); - if (!def) throw new InternalCompilerError(`emitter bug: truthiness of unknown union ${unionId}`); - const name = `sc_ut_${emitter.unionTruthyFns.size}`; - emitter.unionTruthyFns.set(unionId, name); - const sig = `static bool ${name}(ScrUnion *v)`; - emitter.walkerProtos.push(`${sig}; /* ToBoolean ${unionId} */`); - const d: string[] = [`${sig} { /* ToBoolean ${unionId} */`, ` switch (v->tag) {`]; - def.arms.forEach((arm, i) => { - switch (arm.kind) { - case "undefinedT": - case "nullT": - d.push(` case ${i}: return false;`); - break; - case "f64": - d.push(` case ${i}: { double x = scr_union_get_f64(v); return x == x && x != 0; }`); - break; - case "bool": - d.push(` case ${i}: return scr_union_get_bool(v);`); - break; - case "string": - d.push(` case ${i}: return ((ScrStr *)scr_union_peek(v))->len != 0;`); - break; - case "bigint": - d.push(` case ${i}: return scr_bigint_truthy((ScrBigInt *)scr_union_peek(v));`); - break; - case "jsval": - d.push(` case ${i}: return scr_jsval_truthy((ScrJsval *)scr_union_peek(v)) != 0;`); - break; - default: - // Arrays, maps, sets, records, objects, functions, promises, - // regexes, ...: JS objects are always truthy — [] and {} included. - d.push(` case ${i}: return true; /* ${arm.kind} */`); - } - }); - d.push( - ` default: scr_trap("scriptc: internal error: invalid union tag\\n");`, - ` }`, - `}`, - ``, - ); - emitter.walkerDefs.push(...d); - return name; - } - -/** The per-union strict-equality helper (interned per unionId, with a - * separate SameValue variant per unionId when Object.is reaches it): - * different tags are never equal; equal tags compare the arm values — - * unit arms equal, f64 with C == (NaN !== NaN, +0 === -0) or SameValue - * (NaN equals NaN, +0 differs from -0) under the sameValue flag, - * strings by bytes, bools by value, ref arms by POINTER identity (JS - * object equality). Borrows both operands. */ - export function unionEqHelper(emitter: CEmitter, unionId: string, sameValue: boolean): string { - const key = sameValue ? `sv:${unionId}` : unionId; - const existing = emitter.unionEqFns.get(key); - if (existing) return existing; - const def = emitter.unionsById.get(unionId); - if (!def) throw new InternalCompilerError(`emitter bug: equality of unknown union ${unionId}`); - const name = `sc_ue_${emitter.unionEqFns.size}`; - emitter.unionEqFns.set(key, name); - const what = sameValue ? "SameValue" : "strict equality"; - const sig = `static bool ${name}(ScrUnion *a, ScrUnion *b)`; - emitter.walkerProtos.push(`${sig}; /* ${what} ${unionId} */`); - const d: string[] = [ - `${sig} { /* ${what} ${unionId} */`, - ` if (a->tag != b->tag) return false;`, - ` switch (a->tag) {`, - ]; - def.arms.forEach((arm, i) => { - switch (arm.kind) { - case "undefinedT": - case "nullT": - d.push(` case ${i}: return true;`); - break; - case "f64": - d.push( - sameValue - ? ` case ${i}: return scr_num_same_value(scr_union_get_f64(a), scr_union_get_f64(b));` - : ` case ${i}: return scr_union_get_f64(a) == scr_union_get_f64(b);`, - ); - break; - case "bool": - d.push(` case ${i}: return scr_union_get_bool(a) == scr_union_get_bool(b);`); - break; - case "string": - d.push( - ` case ${i}: return scr_str_eq((ScrStr *)scr_union_peek(a), (ScrStr *)scr_union_peek(b));`, - ); - break; - case "bigint": - d.push(` case ${i}: return scr_bigint_eq((ScrBigInt *)scr_union_peek(a), (ScrBigInt *)scr_union_peek(b));`); - break; - default: - // Ref arms: pointer identity, exactly JS object equality. - d.push(` case ${i}: return scr_union_peek(a) == scr_union_peek(b); /* ${arm.kind} */`); - } - }); - d.push( - ` default: scr_trap("scriptc: internal error: invalid union tag\\n");`, - ` }`, - `}`, - ``, - ); - emitter.walkerDefs.push(...d); - return name; - } - -/** The per-union ToString helper (interned per unionId): switch on the - * runtime tag — unit arms return the interned "undefined"/"null" texts, - * string arms retain the payload, f64/bool arms go through the JS-exact - * formatters (String(x) semantics). Ref arms never arrive (the frontend - * fences unions with object arms from string conversion — JS would print - * "[object Object]"). Borrows the operand; the result is owned (+1). */ - export function unionToStrHelper(emitter: CEmitter, unionId: string): string { - const existing = emitter.unionToStrFns.get(unionId); - if (existing) return existing; - const def = emitter.unionsById.get(unionId); - if (!def) throw new InternalCompilerError(`emitter bug: ToString of unknown union ${unionId}`); - const name = `sc_us_${emitter.unionToStrFns.size}`; - emitter.unionToStrFns.set(unionId, name); - const sig = `static ScrStr *${name}(ScrUnion *v)`; - emitter.walkerProtos.push(`${sig}; /* ToString ${unionId} */`); - const d: string[] = [`${sig} { /* ToString ${unionId} */`, ` switch (v->tag) {`]; - def.arms.forEach((arm, i) => { - switch (arm.kind) { - case "undefinedT": - case "nullT": { - const lit = emitter.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null"); - d.push(` case ${i}: return scr_str_retain((ScrStr *)&${lit});`); - break; - } - case "string": - d.push(` case ${i}: return scr_str_retain((ScrStr *)scr_union_peek(v));`); - break; - case "f64": - d.push(` case ${i}: return scr_f64_to_scrstr(scr_union_get_f64(v));`); - break; - case "bool": - d.push(` case ${i}: return scr_bool_to_scrstr(scr_union_get_bool(v));`); - break; - case "bigint": - d.push(` case ${i}: return scr_bigint_to_string((ScrBigInt *)scr_union_peek(v), 10);`); - break; - case "bytes": { - // Buffer.toString() IS the utf8 decode (Node's default - // encoding) — the `Buffer | string` chunk idiom. - const enc = emitter.internLiteral("utf8"); - d.push(` case ${i}: return scr_bytes_to_str((ScrBytes *)scr_union_peek(v), (ScrStr *)&${enc});`); - break; - } - default: - throw new InternalCompilerError(`emitter bug: ToString of union with a ${arm.kind} arm (frontend fences these)`); - } - }); - d.push( - ` default: scr_trap("scriptc: internal error: invalid union tag\\n");`, - ` }`, - `}`, - ``, - ); - emitter.walkerDefs.push(...d); - return name; - } - -/** The per-union array-join helper (interned per unionId): Array.prototype - * .join over union elements — undefined/null arms print EMPTY (exactly - * JS's join, which silences only the nullish elements), every other arm - * goes through the per-union ToString walker (string arms verbatim, - * f64/bool via the JS-exact formatters). The frontend admits only unions - * of f64/string/bool/unit arms here. Borrows the array and separator; - * the result is owned (+1). */ - export function unionJoinHelper(emitter: CEmitter, unionId: string): string { - const existing = emitter.unionJoinFns.get(unionId); - if (existing) return existing; - const def = emitter.unionsById.get(unionId); - if (!def) throw new InternalCompilerError(`emitter bug: join of unknown union ${unionId}`); - const name = `sc_uj_${emitter.unionJoinFns.size}`; - emitter.unionJoinFns.set(unionId, name); - const toStr = unionToStrHelper(emitter, unionId); - const unitTags = def.arms.flatMap((a, i) => - a.kind === "undefinedT" || a.kind === "nullT" ? [i] : [], - ); - const sig = `static ScrStr *${name}(ScrArr *a, ScrStr *sep)`; - emitter.walkerProtos.push(`${sig}; /* join ${unionId} */`); - emitter.walkerDefs.push( - `${sig} { /* Array#join over ${unionId}: nullish arms print empty */`, - ` ScrJsonBuf b;`, - ` scr_jb_init(&b);`, - ` size_t n = (size_t)scr_arr_len(a);`, - ` for (size_t i = 0; i < n; i++) {`, - ` if (i) for (size_t j = 0; j < sep->len; j++) scr_jb_putc(&b, sep->data[j]);`, - ` /* Holes and payload-free present undefined both stringify empty. */`, - ` if (scr_arr_state(a, (double)i) != SCR_ARR_VALUE) continue;`, - ` ScrUnion *u = (ScrUnion *)scr_arr_get_ref(a, (double)i);`, - ...(unitTags.length > 0 - ? [` if (${unitTags.map((t) => `u->tag == ${t}`).join(" || ")}) { scr_union_release(u); continue; }`] - : []), - ` ScrStr *s = ${toStr}(u);`, - ` for (size_t j = 0; j < s->len; j++) scr_jb_putc(&b, s->data[j]);`, - ` scr_str_release(s);`, - ` scr_union_release(u);`, - ` }`, - ` return scr_jb_finish(&b);`, - `}`, - ``, - ); - return name; - } - -/** The dyn ToString helper pair (interned once, type-independent): - * Node's String() over a dyn value — "undefined"/"null" texts, bools, - * JS-exact number formatting (NaN/Infinity spelled out — NOT the JSON - * null), strings verbatim, arrays via Array.prototype.toString (join - * with ","; null/undefined ELEMENTS print empty, nested arrays flatten - * through the recursion — JS-exact), plain objects as - * "[object Object]". The operand is borrowed; the result is owned (+1). */ - export function dynToStrHelper(emitter: CEmitter): string { - if (emitter.dynToStrFn) return emitter.dynToStrFn; - const name = "sc_ds"; - emitter.dynToStrFn = name; - emitter.walkerProtos.push( - `static void ${name}_buf(ScrJsonBuf *b, const ScrDyn *d); /* String(unknown) walker */`, - `static ScrStr *${name}(const ScrDyn *d); /* String(unknown) */`, - ); - emitter.walkerDefs.push( - `static void ${name}_buf(ScrJsonBuf *b, const ScrDyn *d) { /* String(unknown), recursive */`, - ` switch (d->kind) {`, - ` case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); break;`, - ` case SCR_DYN_NULL: scr_jb_puts(b, "null"); break;`, - ` case SCR_DYN_BOOL: scr_jb_puts(b, d->v.b ? "true" : "false"); break;`, - ` case SCR_DYN_BIGINT: {`, - ` ScrStr *s = scr_bigint_to_string(d->v.bigint, 10);`, - ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(b, s->data[i]);`, - ` scr_str_release(s);`, - ` break;`, - ` }`, - ` case SCR_DYN_NUM: {`, - ` ScrStr *s = scr_f64_to_scrstr(d->v.num); /* String(n): NaN/Infinity spelled out, not JSON null */`, - ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(b, s->data[i]);`, - ` scr_str_release(s);`, - ` break;`, - ` }`, - ` case SCR_DYN_STR:`, - ` for (size_t i = 0; i < d->v.str->len; i++) scr_jb_putc(b, d->v.str->data[i]);`, - ` break;`, - ` case SCR_DYN_ARR:`, - ` /* Array.prototype.toString: join(",") — null/undefined ELEMENTS`, - ` * print empty (unlike top level), nested arrays flatten. */`, - ` for (size_t i = 0; i < d->v.arr.len; i++) {`, - ` if (i > 0) scr_jb_putc(b, ',');`, - ` const ScrDyn *e = d->v.arr.items[i];`, - ` if (e->kind == SCR_DYN_UNDEF || e->kind == SCR_DYN_NULL) continue;`, - ` ${name}_buf(b, e);`, - ` }`, - ` break;`, - ` case SCR_DYN_OBJ:`, - ` if (scr_dyn_obj_get(d, "%error", 6)) {`, - ` /* The checked-dynamic tree's error encoding (caughtToDyn): Error.prototype`, - ` * .toString over the encoded name/message — Node's String(err),`, - ` * which carries no stack either. */`, - ` const ScrDyn *en = scr_dyn_obj_get(d, "name", 4);`, - ` const ScrDyn *em = scr_dyn_obj_get(d, "message", 7);`, - ` const ScrStr *ens = (en && en->kind == SCR_DYN_STR) ? en->v.str : NULL;`, - ` const ScrStr *ems = (em && em->kind == SCR_DYN_STR) ? em->v.str : NULL;`, - ` if (ens) for (size_t i = 0; i < ens->len; i++) scr_jb_putc(b, ens->data[i]);`, - ` if (ens && ens->len && ems && ems->len) scr_jb_puts(b, ": ");`, - ` if (ems) for (size_t i = 0; i < ems->len; i++) scr_jb_putc(b, ems->data[i]);`, - ` break;`, - ` }`, - ` scr_jb_puts(b, "[object Object]");`, - ` break;`, - ` case SCR_DYN_BYTES: {`, - ` /* Buffer-flavored values (stream chunks) coerce utf8 (Node's`, - ` * Buffer.toString); plain Uint8Array joins its elements. */`, - ` if (d->buffer) {`, - ` ScrStr *enc = scr_str_new("utf8", 4);`, - ` ScrStr *txt = scr_bytes_to_str(d->v.bytes, enc);`, - ` scr_str_release(enc);`, - ` for (size_t i = 0; i < txt->len; i++) scr_jb_putc(b, txt->data[i]);`, - ` scr_str_release(txt);`, - ` break;`, - ` }`, - ` for (size_t i = 0; i < d->v.bytes->len; i++) {`, - ` if (i > 0) scr_jb_putc(b, ',');`, - ` ScrStr *n = scr_f64_to_scrstr(scr_bytes_get(d->v.bytes, (double)i));`, - ` for (size_t j = 0; j < n->len; j++) scr_jb_putc(b, n->data[j]);`, - ` scr_str_release(n);`, - ` }`, - ` break;`, - ` }`, - ` case SCR_DYN_FUNC:`, - ` /* Function.prototype.toString: the SOURCE is gone in a compiled`, - ` * program, so the native-code form — exactly what JS engines`, - ` * print for their own non-JS functions. */`, - ` scr_jb_puts(b, "function ");`, - ` if (d->v.fn.name) scr_jb_puts(b, d->v.fn.name);`, - ` scr_jb_puts(b, "() { [native code] }");`, - ` break;`, - ` case SCR_DYN_HANDLE: {`, - ` ScrStr *s = scr_dyn_to_string(d, NULL);`, - ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(b, s->data[i]);`, - ` scr_str_release(s);`, - ` break;`, - ` }`, - ` case SCR_DYN_PROMISE:`, - ` /* Object.prototype.toString with the Promise @@toStringTag. */`, - ` scr_jb_puts(b, "[object Promise]");`, - ` break;`, - ` case SCR_DYN_JSVAL:`, - ` /* Island-held: the engine's own ToString (a bridged failure`, - ` * leaves the exception pending and appends nothing). */`, - ` scr_dyn_isl_tostr_buf(b, d);`, - ` break;`, - ` case SCR_DYN_TYPED_REF: {`, - ` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`, - ` ${name}_buf(b, sc_materialized);`, - ` scr_dyn_release(sc_materialized);`, - ` break;`, - ` }`, - ` case SCR_DYN_PROXY:`, - ` scr_dyn_proxy_unsupported("string conversion");`, - ` break;`, - ` }`, - `}`, - `static ScrStr *${name}(const ScrDyn *d) { /* String(unknown) -> owned (+1) */`, - ` if (d->kind == SCR_DYN_STR) return scr_str_retain(d->v.str);`, - ` ScrJsonBuf b;`, - ` scr_jb_init(&b);`, - ` ${name}_buf(&b, d);`, - ` return scr_jb_finish(&b);`, - `}`, - ``, - ); - return name; - } - -/** The caught→dyn converter (interned once, type-independent): a catch - * binding flowing into an `unknown` slot — the typed→unknown deep-copy - * stance over the exception snapshot's runtime kind. Scalar payloads - * convert exactly; an Error-family OBJ payload becomes the checked-dynamic tree's error - * encoding — the reserved "%error" marker plus name/message (and code - * when stamped), so `instanceof Error`, the %Error extraction, and - * String() answer like Node; every other payload (REF — records, arrays, - * closures, unions — and non-Error hierarchy objects, type-erased at - * runtime) becomes an EMPTY dyn object (SEMANTICS.md 67). Borrows the - * snapshot; the result is a fresh tree (+1). Never throws. */ - export function caughtToDynHelper(emitter: CEmitter): string { - if (emitter.caughtToDynFn) return emitter.caughtToDynFn; - const name = "sc_cd"; - emitter.caughtToDynFn = name; - const sig = `static ScrDyn *${name}(const ScrCaught *c)`; - emitter.walkerProtos.push(`${sig}; /* caught -> unknown */`); - emitter.walkerDefs.push( - `${sig} { /* caught -> unknown (+1, fresh tree) */`, - ` switch (c->kind) {`, - ` case SCR_EXC_F64: return scr_dyn_new_num(c->f64);`, - ` case SCR_EXC_BOOL: return scr_dyn_new_bool(c->b);`, - ` case SCR_EXC_STR: return scr_dyn_new_str((ScrStr *)c->payload); /* _new_str retains */`, - ` case SCR_EXC_OBJ:`, - ` if (scr_error_is(c->payload)) {`, - ` /* The identity-cached crossing (scr_json.c): the SAME error`, - ` * instance boxes to ONE dyn node however it crosses, so a`, - ` * caught dyn compares reference-equal to the thrown error's`, - ` * other boxings, like Node. */`, - ` return scr_dyn_from_error((const ScrError *)c->payload);`, - ` }`, - ` /* FALLTHROUGH: a non-Error hierarchy object is type-erased */`, - ` case SCR_EXC_REF:`, - ` case SCR_EXC_PRIMITIVE_REF:`, - ` /* A thrown dyn value passes back BY REFERENCE (identity with`, - ` * every other holder of the node — the traced-throw shape). */`, - ` if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload);`, - ` /* FALLTHROUGH */`, - ` default:`, - ` /* non-dyn REF and non-Error objects: the "[object Object]"`, - ` * approximation — truthy, typeof "object", fields unreadable. */`, - ` return scr_dyn_new_obj();`, - ` }`, - `}`, - ``, - ); - return name; - } - -/** The pretty-print re-indenter (interned once, type-independent): - * `JSON.stringify(v, null, space)` output as a REWRITE of the compact - * serializer text — Node's gap algorithm exactly. Structural '{'/'[' open - * a newline + one-deeper indent (unless immediately closed: `{}` and `[]` - * stay inline, like Node), '}'/']' close onto their own line at the outer - * depth, ',' breaks the line at the current depth, and the key ':' gains - * one space. String state (with escape skipping) keeps literal braces, - * commas, and colons inside JSON strings untouched. Compact input is - * BORROWED; the result is a fresh string (+1). An empty indent (space 0, - * "", null) never reaches here — the frontend drops the property. */ - export function jsonIndentHelper(emitter: CEmitter): string { - if (emitter.jsonIndentFn) return emitter.jsonIndentFn; - const name = "sc_ji"; - emitter.jsonIndentFn = name; - const sig = `static ScrStr *${name}(ScrStr *compact, const char *indent, size_t ilen)`; - emitter.walkerProtos.push(`${sig}; /* stringify space re-indent */`); - emitter.walkerDefs.push( - `${sig} { /* stringify space re-indent (Node's gap algorithm) */`, - ` ScrJsonBuf b;`, - ` scr_jb_init(&b);`, - ` const char *s = compact->data;`, - ` size_t n = compact->len;`, - ` size_t depth = 0;`, - ` bool instr = false;`, - `#define SCR_JI_INDENT() do { \\`, - ` scr_jb_putc(&b, '\\n'); \\`, - ` for (size_t td = 0; td < depth; td++) \\`, - ` for (size_t tk = 0; tk < ilen; tk++) scr_jb_putc(&b, indent[tk]); \\`, - ` } while (0)`, - ` for (size_t i = 0; i < n; i++) {`, - ` char c = s[i];`, - ` if (instr) {`, - ` scr_jb_putc(&b, c);`, - ` if (c == '\\\\' && i + 1 < n) { scr_jb_putc(&b, s[++i]); }`, - ` else if (c == '"') instr = false;`, - ` continue;`, - ` }`, - ` switch (c) {`, - ` case '"': instr = true; scr_jb_putc(&b, c); break;`, - ` case '{': case '[':`, - ` scr_jb_putc(&b, c);`, - ` if (i + 1 < n && s[i + 1] == (c == '{' ? '}' : ']')) {`, - ` scr_jb_putc(&b, s[++i]); /* empty {} / [] stay inline, like Node */`, - ` break;`, - ` }`, - ` depth++;`, - ` SCR_JI_INDENT();`, - ` break;`, - ` case '}': case ']':`, - ` depth--;`, - ` SCR_JI_INDENT();`, - ` scr_jb_putc(&b, c);`, - ` break;`, - ` case ',':`, - ` scr_jb_putc(&b, ',');`, - ` SCR_JI_INDENT();`, - ` break;`, - ` case ':':`, - ` scr_jb_putc(&b, ':');`, - ` scr_jb_putc(&b, ' ');`, - ` break;`, - ` default:`, - ` scr_jb_putc(&b, c);`, - ` }`, - ` }`, - `#undef SCR_JI_INDENT`, - ` return scr_jb_finish(&b);`, - `}`, - ``, - ); - return name; - } - -export function jsonWriteHelper(emitter: CEmitter, t: IrType): string { - const key = typeKey(t); - const existing = emitter.jsonWriters.get(key); - if (existing) return existing; - const name = `sc_jw_${emitter.jsonWriters.size}`; - emitter.jsonWriters.set(key, name); - const sig = `static void ${name}(ScrJsonBuf *b, ${cDecl(t, "v")})`; - emitter.walkerProtos.push(`${sig}; /* stringify ${key} */`); - const d: string[] = [`${sig} { /* stringify ${key} */`]; - switch (t.kind) { - case "f64": - d.push(` scr_jb_put_f64(b, v); /* NaN/Infinity -> null, -0 -> 0, like JS */`); - break; - case "bool": - d.push(` scr_jb_puts(b, v ? "true" : "false");`); - break; - case "string": - d.push(` scr_jb_put_json_str(b, v);`); - break; - case "record": { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: jsonStringify of unknown shape ${t.shapeId}`); - // CYCLE-CAPABLE shapes (recursive record types — the collector- - // fixpoint set) bracket the walk with the circular-detection - // stack: a value already being serialized above throws V8's exact - // circular-structure TypeError (scr_jb_enter). Edge labels stamp - // only before members whose walk can re-enter (cycle-capable - // types); acyclic shapes pay nothing. - const cyclic = emitter.traceAdapterC(t) !== null; - const edgeable = (ft: IrType): boolean => cyclic && emitter.traceAdapterC(ft) !== null; - if (cyclic) d.push(` if (!scr_jb_enter(b, v, ${shape.tuple ? "true" : "false"})) return; /* circular: pending TypeError */`); - // A tuple serializes as a JSON ARRAY in index order — JS-exact - // (JSON.stringify(["a", 1]) is `["a",1]`). Every position is - // required, so labels/commas are static like all-required records. - if (shape.tuple) { - const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name)); - d.push(` scr_jb_putc(b, '[');`); - byIndex.forEach((f, i) => { - if (i > 0) d.push(` scr_jb_putc(b, ',');`); - if (edgeable(f.type)) d.push(` scr_jb_edge_idx(b, ${i});`); - d.push(` ${emitter.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`); - }); - d.push(` scr_jb_putc(b, ']');`); - if (cyclic) d.push(` scr_jb_leave(b);`); - break; - } - // Fields serialize in DECLARED order — JS insertion order, exactly - // Node for objects constructed in declaration order (SEMANTICS.md - // 36 documents the divergence when they are not) — never the - // canonical (sorted) struct order. declaredOrder may omit internal - // '%'-fields (Dirent's %dtype): those are hidden from JSON exactly - // like every other key-order surface. Shapes interned WITHOUT an - // order (the CJS export-table literal, accessor-narrowed JS - // shapes) keep the canonical order — their construction paths - // document the divergence. - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) { - throw new InternalCompilerError(`emitter bug: declaredOrder of shape ${t.shapeId} omits a non-internal field`); - } - const byName = new Map(shape.fields.map((f) => [f.name, f])); - const emitFields = order.map((n) => byName.get(n)).filter((f) => f !== undefined); - // Optional-flavored fields (undefined-armed unions) DROP from the - // output while they hold the undefined arm — exactly Node — so as - // soon as one exists, comma placement turns dynamic (a `first` - // flag); all-required shapes keep the static prefix labels. An - // overflow portion forces the dynamic path too (entry count is - // runtime state). - const droppable = - emitFields.some((f) => undefinedArmTag(f.type, emitter.unionsById) >= 0) || !!shape.indexValue; - d.push(` scr_jb_putc(b, '{');`); - if (!droppable) { - emitFields.forEach((f, i) => { - const label = cStringLiteral(Buffer.from(`${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`, "utf8")); - d.push(` scr_jb_puts(b, ${label});`); - if (edgeable(f.type)) d.push(` scr_jb_edge_prop(b, ${cStringLiteral(Buffer.from(f.name, "utf8"))});`); - // Refcounted fields are BORROWED straight off the struct (the - // record itself is borrowed for the whole call). - d.push(` ${emitter.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`); - }); - } else { - d.push(` bool first = true;`); - for (const f of emitFields) { - const label = cStringLiteral(Buffer.from(jsonObjectKeyLabel(f.name), "utf8")); - const utag = undefinedArmTag(f.type, emitter.unionsById); - const pad = utag >= 0 ? " " : " "; - if (utag >= 0) { - d.push(` if (v->${mangleField(f.name)}->tag != ${utag}) { /* undefined-valued field: dropped, like Node */`); - } - d.push(`${pad}if (!first) scr_jb_putc(b, ',');`); - d.push(`${pad}first = false;`); - d.push(`${pad}scr_jb_puts(b, ${label});`); - if (edgeable(f.type)) d.push(`${pad}scr_jb_edge_prop(b, ${cStringLiteral(Buffer.from(f.name, "utf8"))});`); - d.push(`${pad}${emitter.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`); - if (utag >= 0) d.push(` }`); - } - // Overflow entries follow the declared fields, themselves in JS - // OWN-KEY order — integer-like keys ascending first, then the - // rest in insertion order (scr_map_keys_js_order, the same - // enumeration Object.keys answers; number-keyed signatures make - // integer-like overflow keys ordinary). SEMANTICS.md documents - // the declared-canonical-then-overflow divergence from JS's one - // interleaved key order. Keys escape like any JSON string; - // undefined-valued entries drop, exactly the optional-field rule. - if (shape.indexValue) { - const iv = shape.indexValue; - const m = `v->${OVERFLOW_MEMBER}`; - d.push(` {`); - d.push(` ScrArr *ks = scr_map_keys_js_order(${m});`); - d.push(` for (size_t i = 0; i < ks->len; i++) {`); - d.push(` ScrStr *k = (ScrStr *)scr_arr_get_ref(ks, (double)i);`); - if (iv.kind === "f64" || iv.kind === "bool") { - d.push(` ${cDecl(iv, "e")} = 0;`); - d.push(` scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, &e);`); - } else { - d.push(` ${cDecl(iv, "e")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`); - } - const skipUndef = - iv.kind === "dyn" - ? `e->kind == SCR_DYN_UNDEF` - : undefinedArmTag(iv, emitter.unionsById) >= 0 - ? `e->tag == ${undefinedArmTag(iv, emitter.unionsById)}` - : null; - if (skipUndef) { - d.push(` if (${skipUndef}) { /* undefined-valued entry: dropped, like Node */`); - d.push(` ${releaseCallC(iv, "e")};`); - d.push(` scr_str_release(k);`); - d.push(` continue;`); - d.push(` }`); - } - d.push(` if (!first) scr_jb_putc(b, ',');`); - d.push(` first = false;`); - d.push(` scr_jb_put_json_str(b, k);`); - d.push(` scr_jb_putc(b, ':');`); - if (edgeable(iv)) d.push(` scr_jb_edge_key(b, k);`); - d.push(` ${emitter.jsonWriteHelper(iv)}(b, e);`); - d.push(` scr_str_release(k);`); - if (isRefCounted(iv)) d.push(` ${releaseCallC(iv, "e")};`); - d.push(` }`); - d.push(` scr_arr_release(ks);`); - d.push(` }`); - } - } - d.push(` scr_jb_putc(b, '}');`); - if (cyclic) d.push(` scr_jb_leave(b);`); - break; - } - case "dyn": - // Overflow values under an `unknown` index signature: the checked-dynamic tree - // serializes itself (runtime walker) — object members holding - // undefined drop, array slots holding undefined print null, - // exactly Node. Bare JSON.stringify of dyn stays frontend-fenced; - // this writer is reachable only through overflow entries. - d.push(` scr_jb_put_dyn(b, v);`); - break; - case "array": { - const elem = t.elem; - const w = emitter.jsonWriteHelper(elem); - // A cycle-capable array (elements can point back at it) joins the - // circular-detection stack exactly like a cycle-capable record. - const cyclic = emitter.traceAdapterC(t) !== null; - if (cyclic) d.push(` if (!scr_jb_enter(b, v, true)) return; /* circular: pending TypeError */`); - d.push(` scr_jb_putc(b, '[');`); - d.push(` for (size_t i = 0; i < v->len; i++) {`); - d.push(` if (i > 0) scr_jb_putc(b, ',');`); - d.push(` if (scr_arr_state(v, (double)i) != SCR_ARR_VALUE) { scr_jb_puts(b, "null"); continue; }`); - if (cyclic) d.push(` scr_jb_edge_idx(b, i);`); - if (elem.kind === "f64") { - d.push(` ${w}(b, scr_arr_get_f64(v, (double)i));`); - } else if (elem.kind === "bool") { - d.push(` ${w}(b, scr_arr_get_bool(v, (double)i));`); - } else { - // _get_ref returns +1; release after writing. - d.push(` ${cDecl(elem, "e")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)i);`); - d.push(` ${w}(b, e);`); - d.push(` ${releaseCallC(elem, "e")};`); - } - d.push(` }`); - d.push(` scr_jb_putc(b, ']');`); - if (cyclic) d.push(` scr_jb_leave(b);`); - break; - } - case "union": { - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: jsonStringify of unknown union ${t.unionId}`); - d.push(` switch (v->tag) {`); - def.arms.forEach((arm, i) => { - if (arm.kind === "nullT") { - // Payload-less arm: JSON.stringify(null) is the text `null`, - // exactly like Node. - d.push(` case ${i}: scr_jb_puts(b, "null"); break;`); - return; - } - if (arm.kind === "undefinedT") { - // Array and tuple slots stringify undefined as null. Ordinary - // record fields drop the key before invoking this writer. - d.push(` case ${i}: scr_jb_puts(b, "null"); break;`); - return; - } - const w = emitter.jsonWriteHelper(arm); - if (arm.kind === "f64") { - d.push(` case ${i}: ${w}(b, scr_union_get_f64(v)); break;`); - } else if (arm.kind === "bool") { - d.push(` case ${i}: ${w}(b, scr_union_get_bool(v)); break;`); - } else { - // Payload is BORROWED out of the box for the write. - d.push(` case ${i}: ${w}(b, (${cType(arm).trim()})scr_union_peek(v)); break;`); - } - }); - d.push(` default: scr_trap("scriptc: internal error: invalid union tag\\n");`); - d.push(` }`); - break; - } - default: - throw new InternalCompilerError(`emitter bug: jsonStringify of non-JSON type ${t.kind}`); - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted match predicate for one dynCheck target type: - * `static bool sc_dm_(const ScrDyn *d)` — does this dyn fit T? - * Never throws, builds nothing; union builders use it to try arms in - * canonical order (first FULL match wins). Records are width-tolerant - * here too: extra keys are ignored, only declared fields are examined. */ - export function dynMatchHelper(emitter: CEmitter, t: IrType): string { - const key = typeKey(t); - const existing = emitter.dynMatchers.get(key); - if (existing) return existing; - const name = `sc_dm_${emitter.dynMatchers.size}`; - emitter.dynMatchers.set(key, name); - const sig = `static bool ${name}(const ScrDyn *d)`; - emitter.walkerProtos.push(`${sig}; /* matches ${key} */`); - const d: string[] = [`${sig} { /* matches ${key} */`]; - if (isRefCounted(t) && t.kind !== "dyn") { - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - d.push( - ` if (scr_dyn_typed_ref_is(d, ${keyLit}, ${Buffer.byteLength(key, "utf8")})) return true;`, - ); - } - if (t.kind !== "dyn") { - d.push( - ` if (d && d->kind == SCR_DYN_TYPED_REF) {`, - ` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`, - ` bool sc_out = ${name}(sc_materialized);`, - ` scr_dyn_release(sc_materialized);`, - ` return sc_out;`, - ` }`, - ); - } - switch (t.kind) { - case "url": - d.push(` return scr_dyn_native_url_is(d);`); - break; - case "regex": - d.push(` return scr_dyn_native_regex_is(d);`); - break; - case "set": - d.push(` return scr_dyn_native_set_is(d);`); - break; - case "bigint": - d.push(` return d->kind == SCR_DYN_BIGINT;`); - break; - case "f64": - d.push(` return d->kind == SCR_DYN_NUM;`); - break; - case "string": - d.push(` return d->kind == SCR_DYN_STR;`); - break; - case "bool": - d.push(` return d->kind == SCR_DYN_BOOL;`); - break; - case "nullT": - // JSON null matches exactly the nullT arm (reachable only as a - // union-arm matcher — bare null is not a valid dynCheck target). - d.push(` return d->kind == SCR_DYN_NULL;`); - break; - case "undefinedT": - // JSON text never parses to undefined, but the checked-dynamic tree can HOLD it now - // (overflow reads/conversions under `unknown` index signatures): - // the undefined dyn value matches exactly the undefined arm. - d.push(` return d->kind == SCR_DYN_UNDEF;`); - break; - case "dyn": - // An `unknown` target: every dyn value fits, undefined included. - d.push(` (void)d;`); - d.push(` return true;`); - break; - case "bytes": - d.push(` return scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)});`); - break; - case "func": - d.push(` return d->kind == SCR_DYN_FUNC;`); - break; - case "classval": - d.push(` return scr_dyn_class_is(d, ${cStringLiteral(Buffer.from(key, "utf8"))});`); - break; - case "object": - if (t.className === "%Error") { - d.push(` return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL;`); - } else { - // Non-error classes match only the exact typed-reference fast - // path above. A plain object (including JSON input) must never - // acquire a class identity structurally. - d.push(` (void)d; return false;`); - } - break; - case "record": { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: dynCheck of unknown shape ${t.shapeId}`); - // A tuple matches a JSON ARRAY of EXACTLY its arity whose elements - // match positionally (arity is part of the type — width tolerance - // is an object-key concept and does not apply). - if (shape.tuple) { - const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name)); - d.push(` if (d->kind != SCR_DYN_ARR || d->v.arr.len != ${byIndex.length}) return false;`); - byIndex.forEach((f, i) => { - d.push(` if (!${emitter.dynMatchHelper(f.type)}(d->v.arr.items[${i}])) return false;`); - }); - d.push(` return true;`); - break; - } - d.push(` if (d->kind != SCR_DYN_OBJ) return false;`); - // dyn ('unknown') fields match ANY value, present or missing (a - // missing key builds the undefined dyn value) — no test to emit. - if (shape.fields.some((f) => f.type.kind !== "dyn")) d.push(` const ScrDyn *m;`); - for (const f of shape.fields) { - if (f.type.kind === "dyn") continue; - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - const keyLen = Buffer.byteLength(f.name, "utf8"); - d.push(` m = scr_dyn_obj_get(d, ${keyLit}, ${keyLen});`); - if (undefinedArmTag(f.type, emitter.unionsById) >= 0) { - // Optional-flavored field: a MISSING key is the undefined arm - // (a match); a PRESENT key must fit the union as usual. - d.push(` if (m && !${emitter.dynMatchHelper(f.type)}(m)) return false;`); - } else { - d.push(` if (!m || !${emitter.dynMatchHelper(f.type)}(m)) return false;`); - } - } - // Index-signature shapes: UNDECLARED keys must fit the overflow - // value type (the builder CAPTURES them — width tolerance became - // width capture). A dyn value type accepts anything; concrete - // types check every extra entry. - if (shape.indexValue && shape.indexValue.kind !== "dyn") { - const skip = shape.fields - .map((f) => { - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - const keyLen = Buffer.byteLength(f.name, "utf8"); - return `(e->key_len == ${keyLen} && memcmp(e->key, ${keyLit}, ${keyLen}) == 0)`; - }) - .join(" || "); - d.push(` for (size_t i = 0; i < d->v.obj.len; i++) {`); - d.push(` const ScrDynEntry *e = &d->v.obj.entries[i];`); - if (shape.fields.length > 0) d.push(` if (${skip}) continue;`); - d.push(` if (!${emitter.dynMatchHelper(shape.indexValue)}(e->value)) return false;`); - d.push(` }`); - } - d.push(` return true;`); - break; - } - case "array": { - const m = emitter.dynMatchHelper(t.elem); - d.push(` if (d->kind != SCR_DYN_ARR) return false;`); - d.push(` for (size_t i = 0; i < d->v.arr.len; i++) {`); - d.push(` if (!${m}(d->v.arr.items[i])) return false;`); - d.push(` }`); - d.push(` return true;`); - break; - } - case "union": { - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: dynCheck of unknown union ${t.unionId}`); - // The undefined arm participates: parsed JSON never contains - // undefined (a MISSING record key was the arm's only source), but - // the checked-dynamic tree can hold the undefined value now — overflow entries - // under `unknown` index signatures — and it matches exactly the - // undefined arm. - const arms = def.arms.map((_, tag) => unionArmMatch(emitter, def, tag)); - d.push(` return ${arms.join(" || ")};`); - break; - } - default: { - const h = DYN_HANDLE_KINDS.get(t.kind); - if (h) { - d.push(` return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == ${h.tag};`); - break; - } - throw new InternalCompilerError(`emitter bug: dynMatch of non-JSON type ${t.kind}`); - } - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted keyed read on a dyn value: - * `static ScrDyn *sc_dyn_key_get(ScrDyn *d, ScrStr *k, bool opt)` — - * OBJ answers the member (+1) or the undefined singleton (own-property - * answer); ARR answers `length` and canonical in-range indices, STR - * answers `length` (UTF-16-exact via scr_str_utf16_len), both - * undefined otherwise; NUM/BOOL/BYTES answer undefined. undefined/null - * receivers THROW the catchable Node-shaped TypeError, or answer the - * undefined singleton when `opt` is set (a `?.` step). Borrows d and k; - * the result is +1. Interned once (the memo key can never collide with - * a typeKey — no IR type spells "%"). */ - /** RequireObjectCoercible with V8's destructuring TypeError (the - * dynDestrCheck node): nullish throws "Cannot destructure 'SPELL' as it - * is undefined." (or "…null."), the property form "Cannot destructure - * property 'PROP' of 'SPELL' …" when firstProp is non-NULL — V8's exact - * strings, source spelling included. Non-nullish values pass silently. */ - export function dynDestrCheckHelper(emitter: CEmitter): string { - const memoKey = "%dynDestrCheck"; - const existing = emitter.dynBuilders.get(memoKey); - if (existing) return existing; - const name = "sc_dyn_destr_check"; - emitter.dynBuilders.set(memoKey, name); - const sig = `static void ${name}(const ScrDyn *d, const char *spell, const char *firstProp)`; - emitter.walkerProtos.push(`${sig}; /* destructuring RequireObjectCoercible */`); - emitter.walkerDefs.push( - `${sig} { /* destructuring RequireObjectCoercible */`, - ` if (d->kind != SCR_DYN_UNDEF && d->kind != SCR_DYN_NULL) return;`, - ` ScrJsonBuf b;`, - ` scr_jb_init(&b);`, - ` if (firstProp) {`, - ` scr_jb_puts(&b, "Cannot destructure property '");`, - ` scr_jb_puts(&b, firstProp);`, - ` scr_jb_puts(&b, "' of '");`, - ` scr_jb_puts(&b, spell);`, - ` scr_jb_puts(&b, "' as it is ");`, - ` } else {`, - ` scr_jb_puts(&b, "Cannot destructure '");`, - ` scr_jb_puts(&b, spell);`, - ` scr_jb_puts(&b, "' as it is ");`, - ` }`, - ` scr_jb_puts(&b, d->kind == SCR_DYN_UNDEF ? "undefined." : "null.");`, - ` scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));`, - `}`, - ``, - ); - return name; - } - - /** GetIterator + first-N steps over a dyn value (the dynIterN node), as - * array destructuring sees it: arrays step by index, strings by CODE - * POINT (the string iterator — astral chars arrive unsplit), Buffers by - * byte; everything else throws V8's exact not-iterable TypeError - * ("undefined is not iterable …", "object null is not iterable …", - * "number 1 is not iterable …", "boolean true …", "object …", - * "function …" — each with the "(cannot read property - * Symbol(Symbol.iterator))" tail). Returns a fresh +1 dyn array of - * exactly n elements, undefined-padded past the end. */ - export function dynIterNHelper(emitter: CEmitter): string { - const memoKey = "%dynIterN"; - const existing = emitter.dynBuilders.get(memoKey); - if (existing) return existing; - const name = "sc_dyn_iter_n"; - emitter.dynBuilders.set(memoKey, name); - const sig = `static ScrDyn *${name}(const ScrDyn *d, size_t n)`; - emitter.walkerProtos.push(`${sig}; /* destructuring GetIterator + N steps */`); - emitter.walkerDefs.push( - `${sig} { /* destructuring GetIterator + N steps */`, - ` if (d->kind == SCR_DYN_TYPED_REF) {`, - ` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`, - ` ScrDyn *sc_out = ${name}(sc_materialized, n);`, - ` scr_dyn_release(sc_materialized);`, - ` return sc_out;`, - ` }`, - ` if (d->kind == SCR_DYN_JSVAL) {`, - ` /* An engine array IS iterable — the not-iterable TypeError below`, - ` * would be a wrong claim. Loud fence (lane dyn-routing-ops). */`, - ` scr_dyn_isl_fence(d, "iteration");`, - ` return NULL;`, - ` }`, - ` if (d->kind != SCR_DYN_ARR && d->kind != SCR_DYN_STR && d->kind != SCR_DYN_BYTES) {`, - ` ScrJsonBuf b;`, - ` scr_jb_init(&b);`, - ` switch (d->kind) {`, - ` case SCR_DYN_UNDEF: scr_jb_puts(&b, "undefined"); break;`, - ` case SCR_DYN_NULL: scr_jb_puts(&b, "object null"); break;`, - ` case SCR_DYN_BOOL: scr_jb_puts(&b, d->v.b ? "boolean true" : "boolean false"); break;`, - ` case SCR_DYN_NUM: {`, - ` scr_jb_puts(&b, "number ");`, - ` ScrStr *s = scr_f64_to_scrstr(d->v.num);`, - ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(&b, s->data[i]);`, - ` scr_str_release(s);`, - ` break;`, - ` }`, - ` case SCR_DYN_FUNC: scr_jb_puts(&b, "function"); break;`, - ` default: scr_jb_puts(&b, "object"); break;`, - ` }`, - ` scr_jb_puts(&b, " is not iterable (cannot read property Symbol(Symbol.iterator))");`, - ` scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));`, - ` return NULL;`, - ` }`, - ` ScrDyn *out = scr_dyn_new_arr();`, - ` for (size_t i = 0; i < n; i++) {`, - ` ScrDyn *item = NULL;`, - ` if (d->kind == SCR_DYN_ARR) {`, - ` item = i < d->v.arr.len ? scr_dyn_retain(d->v.arr.items[i]) : scr_dyn_retain(scr_dyn_undefined());`, - ` } else if (d->kind == SCR_DYN_BYTES) {`, - ` item = i < d->v.bytes->len ? scr_dyn_new_num(scr_bytes_get(d->v.bytes, (double)i)) : scr_dyn_retain(scr_dyn_undefined());`, - ` } else {`, - ` /* String iteration: whole code POINTS (astral chars arrive`, - ` * unsplit — the string iterator, not charAt). */`, - ` double len = scr_str_utf16_len(d->v.str);`, - ` double at = 0;`, - ` for (size_t step = 0; step < i && at < len; step++) {`, - ` ScrStr *cp = scr_str_cp_at(d->v.str, at);`, - ` at += scr_str_utf16_len(cp);`, - ` scr_str_release(cp);`, - ` }`, - ` if (at < len) {`, - ` ScrStr *cp = scr_str_cp_at(d->v.str, at);`, - ` item = scr_dyn_new_str(cp);`, - ` scr_str_release(cp);`, - ` } else {`, - ` item = scr_dyn_retain(scr_dyn_undefined());`, - ` }`, - ` }`, - ` scr_dyn_arr_push(out, item); /* push takes ownership */`, - ` }`, - ` return out;`, - `}`, - ``, - ); - return name; - } - - export function dynKeyGetHelper(emitter: CEmitter): string { - const memoKey = "%dynKeyGet"; - const existing = emitter.dynBuilders.get(memoKey); - if (existing) return existing; - const name = "sc_dyn_key_get"; - emitter.dynBuilders.set(memoKey, name); - const sig = `static ScrDyn *${name}(ScrDyn *d, ScrStr *k, bool opt)`; - emitter.walkerProtos.push(`${sig}; /* d[k] on dyn */`); - const d: string[] = [`${sig} { /* d[k] on dyn */`]; - d.push(` if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) {`); - d.push(` if (opt) return scr_dyn_retain(scr_dyn_undefined());`); - d.push(` const char *base = d->kind == SCR_DYN_UNDEF`); - d.push(` ? "Cannot read properties of undefined (reading '"`); - d.push(` : "Cannot read properties of null (reading '";`); - d.push(` ScrStr *head = scr_str_new(base, strlen(base));`); - d.push(` ScrStr *withKey = scr_str_concat(head, k);`); - d.push(` scr_str_release(head);`); - d.push(` ScrStr *tail = scr_str_new("')", 2);`); - d.push(` ScrStr *msg = scr_str_concat(withKey, tail);`); - d.push(` scr_str_release(withKey);`); - d.push(` scr_str_release(tail);`); - d.push(` scr_throw_error(SCR_ERR_TYPE, msg); /* takes ownership */`); - d.push(` return NULL;`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_TYPED_REF) {`); - d.push(` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`); - d.push(` ScrDyn *sc_out = ${name}(sc_materialized, k, opt);`); - d.push(` scr_dyn_release(sc_materialized);`); - d.push(` return sc_out;`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_OBJ) {`); - d.push(` return scr_dyn_obj_read(d, k->data, k->len);`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_PROXY) return scr_dyn_proxy_get(d, k);`); - d.push(` if (d->kind == SCR_DYN_JSVAL) {`); - d.push(` /* Island-held: o[k] reads the REAL engine property (getters`); - d.push(` * included, throws bridged catchably) and the result wraps`); - d.push(` * back scalar-normalized — the routed keyed read that retired`); - d.push(` * the fence (and before it, the fence box's .length -> 0). */`); - d.push(` return scr_dyn_isl_key_get(d, k);`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_HANDLE) {`); - d.push(` /* Native handles: the tag's modeled properties (req.url,`); - d.push(` * res.statusCode, ...) answer boxed values through the`); - d.push(` * installed ops; the rest is the undefined singleton or the`); - d.push(` * loud not-supported ladder (scr_json.c). */`); - d.push(` return scr_dyn_handle_key_get(d, k);`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_BYTES) return scr_dyn_bytes_key_get(d, k);`); - d.push(` if (d->kind == SCR_DYN_FUNC) {`); - d.push(` /* own props (defineProperties writes), then name/length —`); - d.push(` * the function-instance members test/common copies. */`); - d.push(` ScrDyn *m = scr_dyn_fn_get(d, k->data, k->len);`); - d.push(` if (m) return m;`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_STR) {`); - d.push(` if (k->len == 6 && memcmp(k->data, "length", 6) == 0) {`); - d.push(` return scr_dyn_new_num(d->kind == SCR_DYN_ARR ? (double)d->v.arr.len : scr_str_utf16_len(d->v.str));`); - d.push(` }`); - d.push(` if (k->len > 0 && !(k->len > 1 && k->data[0] == '0')) {`); - d.push(` /* canonical index: digits only, no leading zero — array`); - d.push(` * elements, and the string's 1-code-unit char ("abc"[1] is`); - d.push(` * "b" in JS; out of range reads as an absent key). */`); - d.push(` size_t idx = 0; bool digits = true;`); - d.push(` for (size_t i = 0; i < k->len; i++) {`); - d.push(` if (k->data[i] < '0' || k->data[i] > '9' || idx > (SIZE_MAX - 9) / 10) { digits = false; break; }`); - d.push(` idx = idx * 10 + (size_t)(k->data[i] - '0');`); - d.push(` }`); - d.push(` if (digits && d->kind == SCR_DYN_ARR && idx < d->v.arr.len) return scr_dyn_retain(d->v.arr.items[idx]);`); - d.push(` if (digits && d->kind == SCR_DYN_STR && (double)idx < scr_str_utf16_len(d->v.str)) {`); - d.push(` ScrStr *c = scr_str_char_at(d->v.str, (double)idx);`); - d.push(` ScrDyn *r = scr_dyn_new_str(c);`); - d.push(` scr_str_release(c);`); - d.push(` return r;`); - d.push(` }`); - d.push(` }`); - d.push(` if (d->kind == SCR_DYN_ARR && d->v.arr.properties)`); - d.push(` return scr_dyn_obj_read(d->v.arr.properties, k->data, k->len);`); - d.push(` }`); - d.push(` return scr_dyn_retain(scr_dyn_undefined());`); - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted checked builder for one dynCheck target type: - * `static T sc_dc_(const ScrDyn *d, const ScrDynPath *path)` — - * validate the checked-dynamic tree against T and BUILD the typed value (+1), or throw a - * catchable, path-annotated TypeError-shaped string through the exception - * cell and return a dummy (0/false/NULL) with the pending flag set. - * Recursive calls propagate a pending exception by releasing the - * partially-built value and returning — the same pending-check discipline - * emitted function bodies follow. */ - export function dynCheckHelper(emitter: CEmitter, t: IrType): string { - const key = typeKey(t); - const existing = emitter.dynBuilders.get(key); - if (existing) return existing; - const name = `sc_dc_${emitter.dynBuilders.size}`; - emitter.dynBuilders.set(key, name); - const sig = `static ${cType(t)}${cType(t).endsWith("*") ? "" : " "}${name}(const ScrDyn *d, const ScrDynPath *path)`; - emitter.walkerProtos.push(`${sig}; /* check ${key} */`); - const want = cStringLiteral(Buffer.from(dynDesc(t, emitter.recordsById, emitter.unionsById), "utf8")); - const d: string[] = [`${sig} { /* check ${key} */`]; - if (isRefCounted(t) && t.kind !== "dyn") { - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - const keyLen = Buffer.byteLength(key, "utf8"); - d.push( - ` if (scr_dyn_typed_ref_is(d, ${keyLit}, ${keyLen})) {`, - ` return (${cType(t).trim()})scr_dyn_typed_ref_unbox(d);`, - ` }`, - ); - if (t.kind !== "union") { - const rc = vAdapters(t); - d.push( - ` if (d && d->kind == SCR_DYN_TYPED_REF) {`, - ` ${cDecl(t, "sc_cached")} = (${cType(t).trim()})scr_dyn_typed_ref_cached_cast(d, ${keyLit}, ${keyLen});`, - ); - if (t.kind !== "record" && t.kind !== "array") { - d.push(` if (sc_cached) return sc_cached;`); - } - d.push( - ` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`, - ` ${cDecl(t, "sc_out")} = ${name}(sc_materialized, path);`, - ` scr_dyn_release(sc_materialized);`, - ); - if (t.kind === "record") { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) { - throw new InternalCompilerError( - `emitter bug: cached typed-ref cast of unknown shape ${t.shapeId}`, - ); - } - d.push(` if (sc_out && sc_cached) {`); - for (const field of shape.fields) { - const member = mangleField(field.name); - d.push( - ` {`, - ` ${cDecl(field.type, "sc_old")} = sc_cached->${member};`, - ` sc_cached->${member} = sc_out->${member};`, - ` sc_out->${member} = sc_old;`, - ` }`, - ); - } - if (shape.indexValue) { - d.push( - ` ScrMap *sc_old = sc_cached->${OVERFLOW_MEMBER};`, - ` sc_cached->${OVERFLOW_MEMBER} = sc_out->${OVERFLOW_MEMBER};`, - ` sc_out->${OVERFLOW_MEMBER} = sc_old;`, - ); - } - d.push( - ` ${releaseCallC(t, "sc_out")};`, - ` return sc_cached;`, - ` }`, - ` if (sc_cached) ${releaseCallC(t, "sc_cached")};`, - ); - } else if (t.kind === "array") { - d.push( - ` if (sc_out && sc_cached) {`, - ` size_t sc_old_len = sc_cached->len;`, - ` size_t sc_old_cap = sc_cached->cap;`, - ` uint64_t *sc_old_data = sc_cached->data;`, - ` sc_cached->len = sc_out->len;`, - ` sc_cached->cap = sc_out->cap;`, - ` sc_cached->data = sc_out->data;`, - ` sc_out->len = sc_old_len;`, - ` sc_out->cap = sc_old_cap;`, - ` sc_out->data = sc_old_data;`, - ` scr_arr_release(sc_out);`, - ` return sc_cached;`, - ` }`, - ` if (sc_cached) scr_arr_release(sc_cached);`, - ); - } - d.push( - ` if (sc_out) {`, - ` scr_dyn_typed_ref_cache_cast((ScrDyn *)d, ${keyLit}, ${keyLen}, sc_out, &${rc.retain}, &${rc.release});`, - ` }`, - ` return sc_out;`, - ` }`, - ); - } - } - switch (t.kind) { - case "url": - d.push(` return scr_dyn_native_url_check(d, path);`); - break; - case "regex": - d.push(` return scr_dyn_native_regex_check(d, path);`); - break; - case "set": - d.push(` if (!scr_dyn_native_set_is(d)) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` return scr_map_retain((ScrMap *)d->v.handle.ptr);`); - break; - case "bigint": - d.push(` if (d->kind != SCR_DYN_BIGINT) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` return scr_bigint_retain(d->v.bigint);`); - break; - case "f64": - d.push(` if (d->kind != SCR_DYN_NUM) { scr_dyn_check_fail(path, ${want}, d); return 0; }`); - d.push(` return d->v.num;`); - break; - case "classval": - d.push(` return scr_dyn_class_check(d, ${cStringLiteral(Buffer.from(key, "utf8"))}, path);`); - break; - case "bool": - d.push(` if (d->kind != SCR_DYN_BOOL) { scr_dyn_check_fail(path, ${want}, d); return false; }`); - d.push(` return d->v.b;`); - break; - case "string": - d.push(` if (d->kind != SCR_DYN_STR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` return scr_str_retain(d->v.str);`); - break; - case "dyn": - // An `unknown` slot passes through as-is. Typed Web-stream transit - // capsules stay wrapped so repeated references keep identity; - // generic operations materialize their one cached detached view. - d.push(` (void)path;`); - d.push(` return scr_dyn_retain((ScrDyn *)d);`); - break; - case "bytes": - // Check the exact element brand before retaining the shared view. - d.push(` if (!scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)})) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` return scr_dyn_bytes_unbox(d);`); - break; - case "object": - // The %Error extraction (an instanceof-Error narrow on unknown): - // validate the checked-dynamic tree's error encoding — the reserved "%error" marker - // caughtToDyn builds — and extract through the runtime's IDENTITY - // CACHE (scr_error_from_dyn): a dyn error that came from a runtime - // ScrError answers that very instance, so out-and-back crossings - // compare reference-equal (the tracing suite's shape); alien - // %error objects rebuild once and cache the pair. - if (t.className === "%Error") { - d.push(` if (d->kind != SCR_DYN_OBJ || !scr_dyn_obj_get(d, "%error", 6)) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` return scr_error_from_dyn(d);`); - } else { - // Exact typed references returned before this switch. Plain dyn - // objects do not carry a class brand, so fail instead of - // manufacturing an instance from matching fields. - d.push(` scr_dyn_check_fail(path, ${want}, d);`); - d.push(` return NULL;`); - } - break; - case "record": { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: dynCheck of unknown shape ${t.shapeId}`); - const rel = (v: string) => releaseCallC(t, v); - // Tuple targets: a JSON ARRAY of exactly the arity, validated and - // extracted positionally with index paths ("$.pairs[3][1]"). - if (shape.tuple) { - const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name)); - const arityWant = cStringLiteral( - Buffer.from(`array of length ${byIndex.length}`, "utf8"), - ); - d.push(` if (d->kind != SCR_DYN_ARR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` if (d->v.arr.len != ${byIndex.length}) { scr_dyn_check_fail(path, ${arityWant}, d); return NULL; }`); - d.push(` ${cDecl(t, "r")} = ${mangleRecordNew(t.shapeId)}();`); - byIndex.forEach((f, i) => { - d.push(` {`); - d.push(` ScrDynPath p = { path, NULL, ${i} };`); - d.push(` r->${mangleField(f.name)} = ${emitter.dynCheckHelper(f.type)}(d->v.arr.items[${i}], &p);`); - d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`); - d.push(` }`); - }); - d.push(` return r;`); - break; - } - d.push(` if (d->kind != SCR_DYN_OBJ) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - if (shape.fields.length === 0 && !shape.indexValue) { - const sourceAccessor = `${emitter.toDynHelper(t)}_source_access`; - const rc = vAdapters(t); - d.push(` {`); - d.push(` ${cDecl(t, "sc_source")} = (${cType(t).trim()})scr_dyn_obj_source_cast(d, &${sourceAccessor}, &${rc.retain});`); - d.push(` if (sc_source) return sc_source;`); - d.push(` }`); - } - d.push(` ${cDecl(t, "r")} = ${mangleRecordNew(t.shapeId)}();`); - for (const f of shape.fields) { - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - const keyLen = Buffer.byteLength(f.name, "utf8"); - const fieldWant = cStringLiteral(Buffer.from(dynDesc(f.type, emitter.recordsById, emitter.unionsById), "utf8")); - // Width tolerance: only declared fields are looked up — extra JSON - // keys are simply never examined (check-and-extract, not shape - // equality). A missing field fails as "got undefined" — except - // optional-flavored fields (undefined-armed unions), where the - // missing key IS the undefined arm: the interned immortal - // instance, no retain owed (rc == SIZE_MAX). - const utag = f.type.kind === "union" ? undefinedArmTag(f.type, emitter.unionsById) : -1; - d.push(` {`); - d.push(` ScrDynPath p = { path, ${keyLit}, 0 };`); - d.push(` const ScrDyn *m = scr_dyn_obj_get(d, ${keyLit}, ${keyLen});`); - if (f.type.kind === "dyn") { - // An `unknown` field: a present key passes through, a missing - // one IS the undefined dyn value (JS's missing-property read). - // Go through the dyn builder so a Web-stream typed-ref capsule - // cannot escape inside a checked record field. - d.push(` r->${mangleField(f.name)} = ${emitter.dynCheckHelper(f.type)}(m ? m : scr_dyn_undefined(), &p);`); - } else if (utag >= 0 && f.type.kind === "union") { - const unit = emitter.unitInstanceRef(f.type.unionId, utag); - d.push(` if (!m) {`); - d.push(` r->${mangleField(f.name)} = ${unit}; /* absent key -> the undefined arm */`); - d.push(` } else {`); - d.push(` r->${mangleField(f.name)} = ${emitter.dynCheckHelper(f.type)}(m, &p);`); - d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`); - d.push(` }`); - } else { - d.push(` if (!m) { scr_dyn_check_fail(&p, ${fieldWant}, NULL); ${rel("r")}; return NULL; }`); - d.push(` r->${mangleField(f.name)} = ${emitter.dynCheckHelper(f.type)}(m, &p);`); - d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`); - } - d.push(` }`); - } - // Index-signature shapes CAPTURE undeclared keys into the overflow - // map (width tolerance became width capture — plain shapes above - // keep ignoring extras). dyn value types retain the checked-dynamic tree subtree - // as-is; concrete types validate each entry with its key path. - if (shape.indexValue) { - const iv = shape.indexValue; - const skip = shape.fields - .map((f) => { - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - const keyLen = Buffer.byteLength(f.name, "utf8"); - return `(e->key_len == ${keyLen} && memcmp(e->key, ${keyLit}, ${keyLen}) == 0)`; - }) - .join(" || "); - d.push(` for (size_t i = 0; i < d->v.obj.len; i++) {`); - d.push(` const ScrDynEntry *e = &d->v.obj.entries[i];`); - if (shape.fields.length > 0) d.push(` if (${skip}) continue;`); - if (iv.kind === "dyn") { - d.push(` ${cDecl(iv, "ev")} = scr_dyn_retain(e->value);`); - } else { - d.push(` ScrDynPath p = { path, e->key, 0 };`); - d.push(` ${cDecl(iv, "ev")} = ${emitter.dynCheckHelper(iv)}(e->value, &p);`); - d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`); - } - d.push(` ScrStr *ek = scr_str_new(e->key, e->key_len);`); - if (iv.kind === "f64" || iv.kind === "bool") { - d.push(` scr_map_set_str_${iv.kind === "f64" ? "f64" : "bool"}(r->${OVERFLOW_MEMBER}, ek, ev);`); - } else { - d.push(` scr_map_set_str_ref(r->${OVERFLOW_MEMBER}, ek, ev);`); - } - d.push(` scr_str_release(ek);`); - d.push(` }`); - } - d.push(` return r;`); - break; - } - case "array": { - const elem = t.elem; - const c = emitter.dynCheckHelper(elem); - d.push(` if (d->kind != SCR_DYN_ARR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` ScrArr *a = ${emitter.arrNewC(elem, "d->v.arr.len")};`); - d.push(` for (size_t i = 0; i < d->v.arr.len; i++) {`); - d.push(` ScrDynPath p = { path, NULL, i };`); - d.push(` ${cDecl(elem, "e")} = ${c}(d->v.arr.items[i], &p);`); - d.push(` if (scr_exc_pending()) { scr_arr_release(a); return NULL; }`); - d.push(` scr_arr_push_${elemAccess(elem)}(a, e);`); - d.push(` }`); - d.push(` return a;`); - break; - } - case "union": { - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: dynCheck of unknown union ${t.unionId}`); - // Literal discriminators select a layout before structural checks. - // Undiscriminated unions retain canonical first-match ordering. - def.arms.forEach((arm, i) => { - const match = unionArmMatch(emitter, def, i); - if (arm.kind === "undefinedT") { - // Parsed JSON never matches here (no undefined in JSON text — - // a MISSING record key builds this arm in the record builder - // above), but the undefined dyn value can arrive from - // `unknown` index-signature overflows and builds the interned - // unit instance exactly like null. - d.push(` if (${match}) {`); - d.push(` return ${emitter.unitInstanceRef(t.unionId, i)};`); - d.push(` }`); - return; - } - if (arm.kind === "nullT") { - // A matched unit arm builds nothing: the result is THE interned - // immortal instance (rc == SIZE_MAX — RC entry points and the - // collector both skip it, so no retain is owed). - d.push(` if (${match}) {`); - d.push(` return ${emitter.unitInstanceRef(t.unionId, i)};`); - d.push(` }`); - return; - } - const c = emitter.dynCheckHelper(arm); - d.push(` if (${match}) {`); - if (arm.kind === "f64") { - d.push(` return scr_union_new_f64(${i}, ${c}(d, path));`); - } else if (arm.kind === "bool") { - d.push(` return scr_union_new_bool(${i}, ${c}(d, path));`); - } else { - const rc = vAdapters(arm); - d.push(` return scr_union_new_ref(${i}, ${c}(d, path), &${rc.retain}, &${rc.release}, ${emitter.traceArgC(arm)});`); - } - d.push(` }`); - }); - // A typed stream capsule may fit one arm EXACTLY; those probes run - // first so the matching arm can preserve object identity. Only when - // no arm matches the producer tag do we snapshot and retry the - // ordinary copy-based structural union check (the compiler's width - // coercion stance, rather than a live narrowed alias). - d.push(` if (d && d->kind == SCR_DYN_TYPED_REF) {`); - { - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - const keyLen = Buffer.byteLength(key, "utf8"); - const rc = vAdapters(t); - d.push(` ${cDecl(t, "sc_cached")} = (${cType(t).trim()})scr_dyn_typed_ref_cached_cast(d, ${keyLit}, ${keyLen});`); - d.push(` ScrDyn *sc_materialized = scr_dyn_typed_ref_materialize(d);`); - d.push(` ${cDecl(t, "sc_out")} = ${name}(sc_materialized, path);`); - d.push(` scr_dyn_release(sc_materialized);`); - d.push(` if (sc_out && sc_cached) {`); - d.push(` size_t sc_cached_rc = sc_cached->rc;`); - d.push(` size_t sc_out_rc = sc_out->rc;`); - d.push(` ScrUnion sc_old = *sc_cached;`); - d.push(` *sc_cached = *sc_out;`); - d.push(` sc_cached->rc = sc_cached_rc;`); - d.push(` *sc_out = sc_old;`); - d.push(` sc_out->rc = sc_out_rc;`); - d.push(` scr_union_release(sc_out);`); - d.push(` return sc_cached;`); - d.push(` }`); - d.push(` if (sc_cached) scr_union_release(sc_cached);`); - d.push(` if (sc_out) {`); - d.push(` scr_dyn_typed_ref_cache_cast((ScrDyn *)d, ${keyLit}, ${keyLen}, sc_out, &${rc.retain}, &${rc.release});`); - d.push(` }`); - d.push(` return sc_out;`); - } - d.push(` }`); - d.push(` scr_dyn_check_fail(path, ${want}, d);`); - d.push(` return NULL;`); - break; - } - case "func": { - // The checked-dynamic function boundary, OUT direction: a - // non-function kind fails like any dynCheck; an IDENTICAL boxed - // signature (the interned typeKey — same key ⇔ same IR type ⇔ - // same ABI) unwraps the closure directly (identity preserved: - // `mustCall(fn)` handed back to a slot of fn's own type IS fn's - // wrapper, retained); anything else wraps in the per-target - // adapter closure, whose caps[0] obj-box owns the dyn value - // (untraced — cycles through dyn never collect, SEMANTICS.md). - const adapter = dynFuncAdapterHelper(emitter, t); - const sigLit = cStringLiteral(Buffer.from(key, "utf8")); - d.push(` if (d->kind != SCR_DYN_FUNC) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`); - d.push(` if (strcmp(d->v.fn.sig, ${sigLit}) == 0) return scr_closure_retain(d->v.fn.clo);`); - d.push(` {`); - d.push(` ScrClosure *a = scr_closure_new((void *)&${adapter}, 1);`); - d.push(` a->caps[0] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v);`); - d.push(` scr_box_set_ref(a->caps[0], scr_dyn_retain((ScrDyn *)d));`); - d.push(` return a;`); - d.push(` }`); - break; - } - default: { - // Runtime HANDLE targets: a tag-checked reference unwrap (+1 — - // identity, no copy; the runtime throws the path-annotated - // TypeError on any other kind or tag). - const h = DYN_HANDLE_KINDS.get(t.kind); - if (h) { - d.push(` return (${cType(t).trim()})scr_dyn_handle_unbox(d, ${h.tag}, path, ${want});`); - break; - } - throw new InternalCompilerError(`emitter bug: dynCheck of non-JSON type ${t.kind}`); - } - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted static→dyn converter for one type: - * `static ScrDyn *sc_td_( v)` — build a fresh dyn value from a - * static one (+1), DEEP-COPYING composites (a dyn value never aliases - * static storage — the jsMarshal stance). Domain: the JSON-safe kinds - * plus undefined-armed unions (the undefined arm becomes the undefined - * dyn singleton) and index-signature records (whose overflow entries - * copy over). The operand is borrowed. Never throws. */ - export function toDynHelper(emitter: CEmitter, t: IrType): string { - const key = typeKey(t); - const existing = emitter.toDynFns.get(key); - if (existing) return existing; - const name = `sc_td_${emitter.toDynFns.size}`; - emitter.toDynFns.set(key, name); - const sig = `static ScrDyn *${name}(${cDecl(t, "v")})`; - emitter.walkerProtos.push(`${sig}; /* to-dyn ${key} */`); - const d: string[] = [`${sig} { /* to-dyn ${key} */`]; - let sourceAccessor: { name: string; release: string } | null = null; - switch (t.kind) { - case "url": - d.push(` return scr_dyn_native_url(v);`); - break; - case "regex": - d.push(` return scr_dyn_native_regex(v);`); - break; - case "set": - d.push(` return scr_dyn_native_set(v);`); - break; - case "bigint": - d.push(` return scr_dyn_new_bigint(v); /* retains v */`); - break; - case "f64": - d.push(` return scr_dyn_new_num(v);`); - break; - case "bool": - d.push(` return scr_dyn_new_bool(v);`); - break; - case "string": - d.push(` return scr_dyn_new_str(v); /* retains v */`); - break; - case "undefinedT": - // A bare unit field (`x: undefined` in a record — the tls - // options-record's explicit-absent member): the dyn unit value. - d.push(` (void)v; return scr_dyn_retain(scr_dyn_undefined());`); - break; - case "nullT": - d.push(` (void)v; return scr_dyn_new_null();`); - break; - case "object": { - const className = t.className; - if (className === "%Error") { - d.push(` return scr_dyn_from_error(v);`); - break; - } - if (!isDynTypedRefType(t)) { - throw new InternalCompilerError(`emitter bug: to-dyn of runtime class ${className}`); - } - { - const adapter = emitter.liveDynRefAdapter(t); - const rc = vAdapters(t); - const key = typeKey(t); - const keyLit = cStringLiteral(Buffer.from(key, "utf8")); - d.push( - ` return scr_dyn_new_typed_ref${emitter.traceAdapterC(t) !== null ? "_traced" : ""}(v, &${rc.retain}, &${rc.release}, ${keyLit}, ${Buffer.byteLength(key, "utf8")}, &${adapter.snapshot}, ${adapter.commit});`, - ); - } - break; - } - case "dyn": - // A dyn member of a converting composite (a dyn record field): the - // dyn value passes through by reference — already a dyn, already - // immutable-through-copies. - d.push(` return scr_dyn_retain(v);`); - break; - case "classval": - d.push(` return scr_dyn_new_class(v, ${cStringLiteral(Buffer.from(key, "utf8"))});`); - break; - case "func": - // Function-valued record/array fields (EventListenerObject's - // handleEvent method) box through the same identity-preserving - // closure bridge as a bare dynFrom function. - d.push(` return ${dynFuncBoxHelper(emitter, t)}(v, NULL);`); - break; - case "bytes": - // Preserve the view, its element brand, and its backing allocation. - d.push(` return scr_dyn_new_bytes(v);`); - break; - case "record": { - const shape = emitter.recordsById.get(t.shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: to-dyn of unknown shape ${t.shapeId}`); - // CYCLE-CAPABLE shapes guard the deep copy: a cyclic value has no - // finite dyn copy, so enter TRAPS on re-entry (SEMANTICS.md — Node - // shares the reference instead of copying). - const cyclicRec = emitter.traceAdapterC(t) !== null; - if (cyclicRec) d.push(` scr_dyn_from_enter(v);`); - if (shape.tuple) { - // A tuple converts as the JSON ARRAY it is everywhere else. - const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name)); - d.push(` ScrDyn *d = scr_dyn_new_arr();`); - for (const f of byIndex) { - d.push(` scr_dyn_arr_push(d, ${emitter.toDynHelper(f.type)}(v->${mangleField(f.name)}));`); - } - if (cyclicRec) d.push(` scr_dyn_from_leave();`); - d.push(` return scr_dyn_mark_snapshot(d);`); - break; - } - const carriesListenerIdentity = shape.fields.some( - (f) => f.name === "handleEvent" && f.type.kind === "func", - ); - if ((shape.fields.length === 0 && !shape.indexValue) || carriesListenerIdentity) { - const rc = vAdapters(t); - sourceAccessor = { - name: `${name}_source_access`, - release: rc.release, - }; - emitter.walkerProtos.push( - `static ScrDyn *${sourceAccessor.name}(void *v, bool materialize); /* record source ${key} */`, - ); - d.push( - ` ScrDyn *d = scr_dyn_new_obj_with_identity(v, &${rc.retain}, &${sourceAccessor.name});`, - ); - } else { - d.push(` ScrDyn *d = scr_dyn_new_obj();`); - } - // Keys insert in DECLARED order — the dyn object's insertion order - // is observable (Object.keys/for-in over checked-dynamic values, - // dyn JSON), so it must be JS's (SEMANTICS.md 36's stance, same as - // the JSON writer above). Internal '%'-fields declaredOrder omits - // still enter the checked-dynamic tree (after the visible keys) so a - // record→dyn→record round trip keeps their data. - { - const byName = new Map(shape.fields.map((f) => [f.name, f])); - const order = shape.declaredOrder ?? shape.fields.map((f) => f.name); - const inOrder = new Set(order); - const dynFields = [ - ...order.map((n) => byName.get(n)).filter((f) => f !== undefined), - ...shape.fields.filter((f) => !inOrder.has(f.name)), - ]; - for (const f of dynFields) { - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - const keyLen = Buffer.byteLength(f.name, "utf8"); - d.push(` scr_dyn_obj_set(d, ${keyLit}, ${keyLen}, ${emitter.toDynHelper(f.type)}(v->${mangleField(f.name)}));`); - } - } - if (shape.indexValue) { - const iv = shape.indexValue; - const m = `v->${OVERFLOW_MEMBER}`; - // JS OWN-KEY order (integer-like keys ascending first) — the same - // enumeration Object.keys and the JSON writer answer. - d.push(` {`); - d.push(` ScrArr *ks = scr_map_keys_js_order(${m});`); - d.push(` for (size_t i = 0; i < ks->len; i++) {`); - d.push(` ScrStr *k = (ScrStr *)scr_arr_get_ref(ks, (double)i);`); - if (iv.kind === "f64" || iv.kind === "bool") { - d.push(` ${cDecl(iv, "sv")} = 0;`); - d.push(` scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, &sv);`); - d.push(` scr_dyn_obj_set(d, k->data, k->len, scr_dyn_new_${iv.kind === "f64" ? "num" : "bool"}(sv));`); - } else if (iv.kind === "dyn") { - // get_str_ref returns +1 — exactly the ownership obj_set takes. - d.push(` scr_dyn_obj_set(d, k->data, k->len, (ScrDyn *)scr_map_get_str_ref(${m}, k));`); - } else { - d.push(` ${cDecl(iv, "e")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`); - d.push(` scr_dyn_obj_set(d, k->data, k->len, ${emitter.toDynHelper(iv)}(e));`); - d.push(` ${releaseCallC(iv, "e")};`); - } - d.push(` scr_str_release(k);`); - d.push(` }`); - d.push(` scr_arr_release(ks);`); - d.push(` }`); - } - if (cyclicRec) d.push(` scr_dyn_from_leave();`); - d.push(` return scr_dyn_mark_snapshot(d);`); - break; - } - case "array": { - const elem = t.elem; - // Cycle-capable arrays guard the deep copy like records above. - const cyclicArr = emitter.traceAdapterC(t) !== null; - if (cyclicArr) d.push(` scr_dyn_from_enter(v);`); - d.push(` ScrDyn *d = scr_dyn_new_arr();`); - d.push(` for (size_t i = 0; i < v->len; i++) {`); - d.push(` if (scr_arr_state(v, (double)i) == SCR_ARR_UNDEFINED) { scr_dyn_arr_push(d, scr_dyn_retain(scr_dyn_undefined())); continue; }`); - if (elem.kind === "f64") { - d.push(` scr_dyn_arr_push(d, scr_dyn_new_num(scr_arr_get_f64(v, (double)i)));`); - } else if (elem.kind === "bool") { - d.push(` scr_dyn_arr_push(d, scr_dyn_new_bool(scr_arr_get_bool(v, (double)i)));`); - } else { - d.push(` ${cDecl(elem, "e")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)i);`); - d.push(` scr_dyn_arr_push(d, ${emitter.toDynHelper(elem)}(e));`); - d.push(` ${releaseCallC(elem, "e")};`); - } - d.push(` }`); - if (cyclicArr) d.push(` scr_dyn_from_leave();`); - d.push(` return scr_dyn_mark_snapshot(d);`); - break; - } - case "union": { - const def = emitter.unionsById.get(t.unionId); - if (!def) throw new InternalCompilerError(`emitter bug: to-dyn of unknown union ${t.unionId}`); - d.push(` switch (v->tag) {`); - def.arms.forEach((arm, i) => { - if (arm.kind === "undefinedT") { - d.push(` case ${i}: return scr_dyn_retain(scr_dyn_undefined());`); - } else if (arm.kind === "nullT") { - d.push(` case ${i}: return scr_dyn_new_null();`); - } else if (arm.kind === "f64") { - d.push(` case ${i}: return scr_dyn_new_num(scr_union_get_f64(v));`); - } else if (arm.kind === "bool") { - d.push(` case ${i}: return scr_dyn_new_bool(scr_union_get_bool(v));`); - } else if (arm.kind === "func") { - // A boxable function arm crosses through the checked-dynamic - // function boundary (the dynFrom func special case, sans name). - d.push(` case ${i}: return ${dynFuncBoxHelper(emitter, arm)}((ScrClosure *)scr_union_peek(v), NULL);`); - } else { - d.push(` case ${i}: return ${emitter.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(v));`); - } - }); - d.push(` default: scr_trap("scriptc: internal error: invalid union tag\\n");`); - d.push(` }`); - break; - } - case "promise": { - // Promises box by REFERENCE (SCR_DYN_PROMISE — identity is the - // promise): promise carries its ScrPromise directly (the - // payload is already a dyn value); any other inner boxes an - // ADAPTER promise whose settle callback converts the payload - // (rejections copy raw inside the runtime's cb-waiter machinery). - if (t.inner.kind === "dyn") { - d.push(` return scr_dyn_new_promise(v);`); - break; - } - d.push(` return scr_dyn_new_promise_adapting(v, &${promiseDynAdapterHelper(emitter, t.inner)});`); - break; - } - default: { - // Runtime HANDLE kinds box by REFERENCE (identity — no copy): - // scr_dyn_new_handle retains the borrowed operand through the - // tag's installed ops. - const h = DYN_HANDLE_KINDS.get(t.kind); - if (h) { - d.push(` return scr_dyn_new_handle(v, ${h.tag});`); - break; - } - throw new InternalCompilerError(`emitter bug: to-dyn of unsupported type ${t.kind}`); - } - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - if (sourceAccessor) { - emitter.walkerDefs.push( - `static ScrDyn *${sourceAccessor.name}(void *v, bool materialize) {`, - ` if (materialize) return ${name}((${cType(t).trim()})v);`, - ` ${sourceAccessor.release}(v);`, - ` return NULL;`, - `}`, - ``, - ); - } - return name; - } - -/** The checked-dynamic tree-promise settle adapter for one fulfillment payload type: - * `static void sc_pda_(ScrPromise *dst, ScrPromise *src)` — read - * src's fulfilled payload by its compile-time kind, convert it to a dyn - * value, and fulfill dst with it (scr_dyn_new_promise_adapting's - * callback; rejections never reach an adapter — the runtime copies them - * raw). Interned per inner typeKey. */ - function promiseDynAdapterHelper(emitter: CEmitter, inner: IrType): string { - const key = typeKey(inner); - const existing = emitter.promiseDynAdapters.get(key); - if (existing) return existing; - const name = `sc_pda_${emitter.promiseDynAdapters.size}`; - emitter.promiseDynAdapters.set(key, name); - const sig = `static void ${name}(ScrPromise *dst, ScrPromise *src)`; - emitter.walkerProtos.push(`${sig}; /* dyn-box settle adapter for promise<${key}> */`); - const d: string[] = [`${sig} { /* dyn-box settle adapter for promise<${key}> */`]; - const fulfill = (expr: string) => - ` scr_promise_fulfill_ref(dst, ${expr}, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);`; - switch (inner.kind) { - case "void": - case "undefinedT": - d.push(` (void)src;`); - d.push(fulfill(`scr_dyn_retain(scr_dyn_undefined())`)); - break; - case "nullT": - d.push(` (void)src;`); - d.push(fulfill(`scr_dyn_new_null()`)); - break; - case "f64": - d.push(fulfill(`scr_dyn_new_num(scr_promise_payload_f64(src))`)); - break; - case "bool": - d.push(fulfill(`scr_dyn_new_bool(scr_promise_payload_bool(src))`)); - break; - case "string": - d.push(` ScrStr *s = scr_promise_payload_str(src);`); - d.push(` ScrDyn *dv = scr_dyn_new_str(s); /* retains s */`); - d.push(` scr_str_release(s);`); - d.push(fulfill(`dv`)); - break; - default: { - // Ref-payload inners (records, arrays, bytes, %Error, unions, - // nested promises, handles): extract (+1 via the stored retain), - // convert through the shared to-dyn spelling, release the extract. - d.push(` ${cDecl(inner, "pv")} = (${cType(inner).trim()})scr_promise_payload_ref(src);`); - d.push(` ScrDyn *dv = ${toDynExprC(emitter, inner, "pv")};`); - if (isRefCounted(inner)) d.push(` ${releaseCallC(inner, "pv")};`); - d.push(fulfill(`dv`)); - break; - } - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/* ── the checked-dynamic function boundary (ir.ts) ───────────────── - * Three interned per-signature helpers: - * - * sc_dfk_ — the CALL THUNK a dyn-boxed closure carries: validate - * each dyn argument into the declared param type (JS arity: extras - * ignored, missing args are the undefined dyn value — a param type - * the undefined value fails checks throws the path-annotated - * TypeError), call through the closure, convert the result back to a - * dyn value (+1). - * - * sc_dfb_ — the BOX builder dynFrom emits: a fresh SCR_DYN_FUNC - * node carrying the retained closure, the thunk, the arity, the - * interned signature key (typeKey — dynCheck's exact-unwrap fast - * path), and the best-effort name. - * - * sc_dfa_ — the ADAPTER a func-targeted dynCheck wraps a - * NON-identical boxed signature in: a closure of the TARGET type - * whose body converts each typed argument INTO dyn, calls the boxed - * thunk, and validates the dyn result into the target's return type. - */ - -/** The dyn argument spelling of one static value (thunk results, adapter - * arguments): dyn passes through (+1 via retain — callers own the - * result uniformly), funcs box (anonymous — the static name is gone by - * the time a value flows through an adapter; SEMANTICS.md), everything - * else rides the toDyn converter. `expr` is BORROWED in every arm. */ - function toDynExprC(emitter: CEmitter, t: IrType, expr: string): string { - if (t.kind === "dyn") return `scr_dyn_retain(${expr})`; - if (t.kind === "func") return `${emitter.dynFuncBoxHelper(t)}(${expr}, NULL)`; - // An island value wraps by reference (scalar-normalizing) — the - // jsval-returning callback shape of the routed-dispatch lane. - if (t.kind === "jsval") return `scr_dyn_from_jsval(${expr})`; - return `${emitter.toDynHelper(t)}(${expr})`; - } - -/** The call thunk for one closure signature (see the block comment). */ - function dynFuncThunkHelper(emitter: CEmitter, t: IrType & { kind: "func" }): string { - const key = typeKey(t); - const existing = emitter.dynFuncThunks.get(key); - if (existing) return existing; - const name = `sc_dfk_${emitter.dynFuncThunks.size}`; - emitter.dynFuncThunks.set(key, name); - const sig = `static ScrDyn *${name}(ScrClosure *c, ScrDyn *const *args, size_t argc)`; - emitter.walkerProtos.push(`${sig}; /* dyn call thunk for ${key} */`); - const d: string[] = [`${sig} { /* dyn call thunk for ${key} */`]; - const typedRest = t.rest === true && t.restAbi === "typed"; - const hiddenRest = t.rest === true && !typedRest; - if (t.params.length === 0) d.push(` (void)args;`); - d.push(` (void)argc;`); - t.params.forEach((p, i) => { - // JS arity: a missing argument IS the undefined dyn value; the - // param's own check decides whether that flies (dyn params take - // anything; a number param throws the catchable TypeError). - d.push(` ${cDecl(p, `a${i}`)};`); - d.push(` {`); - const packsRest = typedRest && i === t.params.length - 1; - if (packsRest) { - d.push(` ScrDyn *ad = scr_dyn_new_arr();`); - d.push(` for (size_t ri = ${i}; ri < argc; ri++) scr_dyn_arr_push(ad, scr_dyn_retain(args[ri]));`); - } else { - d.push(` const ScrDyn *ad = ${i} < argc ? args[${i}] : scr_dyn_undefined();`); - } - if (p.kind === "dyn") { - d.push(` a${i} = scr_dyn_retain((ScrDyn *)ad);`); - } else if (p.kind === "jsval") { - // A checker-'any' param: the dyn argument enters the island — - // wrapped cells unwrap by reference, data deep-copies, boxed - // functions cross through the host shim; a kind with no crossing - // throws the catchable TypeError (NULL + pending). - d.push(` a${i} = scr_jsval_from_dyn(ad);`); - const undo = t.params - .slice(0, i) - .flatMap((q, j) => (isRefCounted(q) ? [`${releaseCallC(q, `a${j}`)};`] : [])); - d.push(` if (!a${i}) { ${undo.join(" ")}${undo.length > 0 ? " " : ""}return NULL; }`); - } else { - if (!packsRest) d.push(` ScrDynPath pp = { NULL, NULL, ${i} };`); - d.push(` a${i} = ${emitter.dynCheckHelper(p)}(ad, ${packsRest ? "NULL" : "&pp"});`); - if (packsRest) d.push(` scr_dyn_release(ad);`); - const undo = t.params - .slice(0, i) - .flatMap((q, j) => (isRefCounted(q) ? [`${releaseCallC(q, `a${j}`)};`] : [])); - d.push(` if (scr_exc_pending()) { ${undo.join(" ")}${undo.length > 0 ? " " : ""}return NULL; }`); - } - d.push(` }`); - }); - // VARIADIC (rest-marked) signatures: one extra trailing dyn-array - // param carries the call's arguments from index params.length on — - // the mustCall wrapper's `arguments`, a JS `...args`. Built fresh per - // call (+1, moved into the callee like every param). - if (hiddenRest) { - d.push(` ScrDyn *rest = scr_dyn_new_arr();`); - d.push(` for (size_t ri = ${t.argumentsAll ? 0 : t.params.length}; ri < argc; ri++) {`); - d.push(` scr_dyn_arr_push(rest, scr_dyn_retain((ScrDyn *)args[ri]));`); - d.push(` }`); - } - // The closure CONSUMES its params (+1 each moved in — exactly what the - // builders above returned). - const castParams = ["ScrClosure *", ...t.params.map((p) => cType(p).trim()), ...(hiddenRest ? ["ScrDyn *"] : [])].join(", "); - const call = `((${cType(t.ret).trim()} (*)(${castParams}))c->fn)(${["c", ...t.params.map((_, i) => `a${i}`), ...(hiddenRest ? ["rest"] : [])].join(", ")})`; - if (t.ret.kind === "void") { - d.push(` ${call};`); - d.push(` if (scr_exc_pending()) return NULL;`); - d.push(` return scr_dyn_retain(scr_dyn_undefined());`); - } else if (t.ret.kind === "dyn") { - d.push(` ScrDyn *r = ${call};`); - d.push(` if (scr_exc_pending()) return NULL;`); - d.push(` return r;`); - } else { - d.push(` ${cDecl(t.ret, "r")} = ${call};`); - d.push(` if (scr_exc_pending()) return NULL;`); - d.push(` ScrDyn *out = ${toDynExprC(emitter, t.ret, "r")};`); - if (isRefCounted(t.ret)) d.push(` ${releaseCallC(t.ret, "r")};`); - d.push(` return out;`); - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The box builder dynFrom emits for one closure signature. */ - export function dynFuncBoxHelper(emitter: CEmitter, t: IrType & { kind: "func" }): string { - const key = typeKey(t); - const existing = emitter.dynFuncBoxes.get(key); - if (existing) return existing; - const name = `sc_dfb_${emitter.dynFuncBoxes.size}`; - emitter.dynFuncBoxes.set(key, name); - const sig = `static ScrDyn *${name}(ScrClosure *v, const char *fname)`; - emitter.walkerProtos.push(`${sig}; /* box ${key} into dyn */`); - const thunk = dynFuncThunkHelper(emitter, t); - const sigLit = cStringLiteral(Buffer.from(key, "utf8")); - emitter.walkerDefs.push( - `${sig} { /* box ${key} into dyn */`, - ` return scr_dyn_new_func(scr_closure_retain(v), &${thunk}, ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ${sigLit}, fname);`, - `}`, - ``, - ); - return name; - } - -/** The adapter closure body for one TARGET signature: the emitted C - * function a func-targeted dynCheck wraps a non-identical boxed - * signature in. caps[0] is an untraced obj-box owning the dyn function - * value. */ - function dynFuncAdapterHelper(emitter: CEmitter, t: IrType & { kind: "func" }): string { - const key = typeKey(t); - const existing = emitter.dynFuncAdapters.get(key); - if (existing) return existing; - const name = `sc_dfa_${emitter.dynFuncAdapters.size}`; - emitter.dynFuncAdapters.set(key, name); - const params = ["ScrClosure *sc_env", ...t.params.map((p, i) => cDecl(p, `a${i}`))].join(", "); - const sig = `static ${cType(t.ret)}${cType(t.ret).endsWith("*") ? "" : " "}${name}(${params})`; - emitter.walkerProtos.push(`${sig}; /* dyn fn adapter to ${key} */`); - const dummy = - t.ret.kind === "void" ? "" : t.ret.kind === "f64" ? "0" : t.ret.kind === "bool" ? "false" : "NULL"; - const d: string[] = [`${sig} { /* dyn fn adapter to ${key} */`]; - d.push(` ScrDyn *sc_fn = (ScrDyn *)scr_box_get_ref(sc_env->caps[0]); /* +1 */`); - // The adapter OWNS its params (closure ABI); each converts to a dyn - // argument (toDynExprC borrows) and releases. - if (t.params.length > 0) { - d.push(` ScrDyn *sc_args[${t.params.length}];`); - t.params.forEach((p, i) => { - d.push(` sc_args[${i}] = ${toDynExprC(emitter, p, `a${i}`)};`); - if (isRefCounted(p)) d.push(` ${releaseCallC(p, `a${i}`)};`); - }); - } - // The kind is FUNC by construction (the dynCheck that built this - // adapter tested it), so `what` is unreachable — spelled anyway. - d.push( - ` ScrDyn *sc_r = scr_dyn_call(sc_fn, ${t.params.length > 0 ? "sc_args" : "NULL"}, ${t.params.length}, "value");`, - ); - d.push(` scr_dyn_release(sc_fn);`); - t.params.forEach((_, i) => d.push(` scr_dyn_release(sc_args[${i}]);`)); - d.push(` if (scr_exc_pending()) return${dummy === "" ? "" : ` ${dummy}`};`); - if (t.ret.kind === "void") { - d.push(` scr_dyn_release(sc_r);`); - d.push(` return;`); - } else if (t.ret.kind === "dyn") { - d.push(` return sc_r;`); - } else { - // Validate the dyn result into the target's return type — a lying - // wrapper throws the catchable TypeError here (path "$"), exactly - // the dynCheck stance; the dummy result rides the pending unwind. - d.push(` ${cDecl(t.ret, "out")} = ${emitter.dynCheckHelper(t.ret)}(sc_r, NULL);`); - d.push(` scr_dyn_release(sc_r);`); - d.push(` return out;`); - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted dynamic-keyed READ helper for one (shape, result type): - * `static sc_rkg_( *r, ScrStr *k)` — declared fields - * answer first (string comparisons in canonical order; the frontend - * proved each surfaces as T: identity, a to-dyn conversion, or an - * arm-into-union wrap), then the overflow map on index-signature shapes. - * A MISSING key yields the undefined dyn value (T dyn), the undefined - * arm (T undefined-armed union), or TRAPS with the key text (the array - * OOB policy — the checker claimed T and no undefined is representable). - * Borrows both; refcounted results are owned (+1). */ - export function recordKeyGetHelper(emitter: CEmitter, shapeId: string, t: IrType, overflowOnly = false): string { - const key = `${shapeId}|${typeKey(t)}${overflowOnly ? "|ovf" : ""}`; - const existing = emitter.recordKeyGetFns.get(key); - if (existing) return existing; - const shape = emitter.recordsById.get(shapeId); - if (!shape) throw new InternalCompilerError(`emitter bug: keyed read of unknown shape ${shapeId}`); - const name = `sc_rkg_${emitter.recordKeyGetFns.size}`; - emitter.recordKeyGetFns.set(key, name); - const struct = mangleRecordStruct(shapeId); - const sig = `static ${cType(t)}${cType(t).endsWith("*") ? "" : " "}${name}(${struct} *r, ScrStr *k)`; - emitter.walkerProtos.push(`${sig}; /* r[k] on ${shapeId} as ${typeKey(t)} */`); - const d: string[] = [`${sig} { /* r[k] on ${shapeId} as ${typeKey(t)} */`]; - // How a value of type `vt` (a field, or the overflow hit) surfaces as T. - const surface = (vt: IrType, expr: string, owned: boolean): string => { - if (typeEquals(vt, t)) { - return owned || !isRefCounted(vt) ? expr : retainCallC(vt, expr); - } - if (t.kind === "dyn") { - // toDyn borrows — an owned operand would leak; callers pass borrowed. - return `${emitter.toDynHelper(vt)}(${expr})`; - } - if (t.kind === "union") { - if (vt.kind === "union") { - const helper = unionWidenHelper(emitter, vt.unionId, t.unionId); - if (!owned) return `${helper}(${expr})`; - d.push(` ScrUnion *widened = ${helper}(${expr});`); - d.push(` scr_union_release(${expr});`); - return "widened"; - } - const def = emitter.unionsById.get(t.unionId); - const tag = def?.arms.findIndex((a) => typeEquals(a, vt)) ?? -1; - if (tag < 0) throw new InternalCompilerError(`emitter bug: keyed read arm for ${typeKey(vt)} in ${typeKey(t)}`); - if (vt.kind === "f64") return `scr_union_new_f64(${tag}, ${expr})`; - if (vt.kind === "bool") return `scr_union_new_bool(${tag}, ${expr})`; - const rc = vAdapters(vt); - const payload = owned ? expr : retainCallC(vt, expr); - return `scr_union_new_ref(${tag}, ${payload}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(vt)})`; - } - throw new InternalCompilerError(`emitter bug: keyed read cannot surface ${typeKey(vt)} as ${typeKey(t)}`); - }; - // overflowOnly (a literal key naming no declared field): the declared - // switch can never hit — only the overflow answers. - for (const f of overflowOnly ? [] : shape.fields) { - const lit = emitter.internLiteral(f.name); - d.push(` if (scr_str_eq(k, (ScrStr *)&${lit})) { /* ${cCommentText(f.name)} */`); - d.push(` return ${surface(f.type, `r->${mangleField(f.name)}`, false)};`); - d.push(` }`); - } - const iv = shape.indexValue; - if (iv) { - const m = `r->${OVERFLOW_MEMBER}`; - if (iv.kind === "f64" || iv.kind === "bool") { - const acc = iv.kind === "f64" ? "f64" : "bool"; - d.push(` ${cDecl(iv, "hit")};`); - d.push(` if (scr_map_get_str_${acc}(${m}, k, &hit)) {`); - d.push(` return ${surface(iv, "hit", true)};`); - d.push(` }`); - } else { - d.push(` ${cDecl(iv, "hit")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`); - d.push(` if (hit) {`); - if (typeEquals(iv, t)) { - d.push(` return hit; /* get returned +1 */`); - } else if (t.kind === "union") { - d.push(` return ${surface(iv, "hit", true)};`); - } else { - throw new InternalCompilerError(`emitter bug: keyed read overflow ${typeKey(iv)} as ${typeKey(t)}`); - } - d.push(` }`); - } - } - // The miss path. - if (t.kind === "dyn") { - d.push(` return scr_dyn_retain(scr_dyn_undefined());`); - } else if (t.kind === "union" && undefinedArmTag(t, emitter.unionsById) >= 0) { - d.push(` return ${emitter.unitInstanceRef(t.unionId, undefinedArmTag(t, emitter.unionsById))};`); - } else { - // JS would answer undefined; T has no way to say it (the checker - // claimed T without noUncheckedIndexedAccess) — trap like an array - // OOB read instead of corrupting a typed slot (SEMANTICS.md). - d.push(` scr_trap_fmt("scriptc: TypeError: record has no key '%.*s' (typed '${dynDesc(t, emitter.recordsById, emitter.unionsById)}' — no undefined is representable)\\n", (int)k->len, k->data);`); - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } - -/** The emitted dynamic-keyed WRITE helper for one index-signature shape: - * `static void sc_rks_( *r, ScrStr *k, v)` — a declared - * key writes THROUGH to the struct slot (dyn values validate against the - * field's type first via the dynCheck walker: a mismatch throws the - * catchable TypeError and leaves the field untouched — JS would store - * anything, the documented divergence; typed values store directly), - * an undeclared key inserts/replaces in the overflow map. Borrows r and - * k; OWNS v (+1 moves in — released on the validation-failure path). */ - export function recordKeySetHelper(emitter: CEmitter, shapeId: string): string { - const existing = emitter.recordKeySetFns.get(shapeId); - if (existing) return existing; - const shape = emitter.recordsById.get(shapeId); - // Signature-free shapes dispatch over their (one-typed) declared - // fields and TRAP on a miss (scr_record_key_miss — JS would add the - // property, which a monomorphic struct cannot); overflow shapes keep - // the map insert tail. - const iv = shape?.indexValue ?? shape?.fields[0]?.type; - if (!shape || !iv) throw new InternalCompilerError(`emitter bug: keyed write on field-free non-overflow shape ${shapeId}`); - const name = `sc_rks_${emitter.recordKeySetFns.size}`; - emitter.recordKeySetFns.set(shapeId, name); - const struct = mangleRecordStruct(shapeId); - const sig = `static void ${name}(${struct} *r, ScrStr *k, ${cDecl(iv, "v")})`; - emitter.walkerProtos.push(`${sig}; /* r[k] = v on ${shapeId} */`); - const d: string[] = [`${sig} { /* r[k] = v on ${shapeId} */`]; - for (const f of shape.fields) { - const lit = emitter.internLiteral(f.name); - const member = `r->${mangleField(f.name)}`; - d.push(` if (scr_str_eq(k, (ScrStr *)&${lit})) { /* ${cCommentText(f.name)} */`); - if (iv.kind === "dyn" && shape.indexValue) { - const keyLit = cStringLiteral(Buffer.from(f.name, "utf8")); - d.push(` ScrDynPath p = { NULL, ${keyLit}, 0 };`); - d.push(` ${cDecl(f.type, "nv")} = ${emitter.dynCheckHelper(f.type)}(v, &p);`); - d.push(` scr_dyn_release(v);`); - d.push(` if (scr_exc_pending()) return; /* mismatched write: TypeError, field untouched */`); - d.push(` ${isRefCounted(f.type) ? `if (${member}) ${releaseCallC(f.type, member)};` : `(void)0;`}`); - d.push(` ${member} = nv;`); - } else { - // typeEquals(f.type, iv) — the frontend fences everything else. - if (isRefCounted(f.type)) { - d.push(` if (${member}) ${releaseCallC(f.type, member)};`); - } - d.push(` ${member} = v;`); - } - d.push(` return;`); - d.push(` }`); - } - if (!shape.indexValue) { - // The MISS on a fixed shape: release the moved-in value, throw the - // catchable TypeError naming the key (JS would add the property — - // the documented monomorphic-struct divergence). - if (isRefCounted(iv)) d.push(` if (v) ${releaseCallC(iv, "v")};`); - d.push(` scr_record_key_miss(k);`); - } else { - const m = `r->${OVERFLOW_MEMBER}`; - if (iv.kind === "f64" || iv.kind === "bool") { - d.push(` scr_map_set_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, v);`); - } else { - d.push(` scr_map_set_str_ref(${m}, k, v); /* v moves in */`); - } - } - d.push(`}`, ``); - emitter.walkerDefs.push(...d); - return name; - } diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index 0dead0815..9ab262b43 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -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); } diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index 38aa064fc..7dc27a56b 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -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 diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 5de00992a..b309826cc 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -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, }; } diff --git a/packages/compiler/src/backend/external-c.ts b/packages/compiler/src/backend/external-c.ts index fe9a5964d..8ecaa6477 100644 --- a/packages/compiler/src/backend/external-c.ts +++ b/packages/compiler/src/backend/external-c.ts @@ -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 { 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 { 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(); - } -} diff --git a/packages/compiler/src/backend/ffi-callbacks.ts b/packages/compiler/src/backend/ffi-callbacks.ts index 93bc5309b..104f6f4e3 100644 --- a/packages/compiler/src/backend/ffi-callbacks.ts +++ b/packages/compiler/src/backend/ffi-callbacks.ts @@ -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 { } /** 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( entry: IrFfiImport, callbackArgs: ReadonlyMap, diff --git a/packages/compiler/src/backend/library-identity-markers.ts b/packages/compiler/src/backend/library-identity-markers.ts index 831609ec1..c0e301e3e 100644 --- a/packages/compiler/src/backend/library-identity-markers.ts +++ b/packages/compiler/src/backend/library-identity-markers.ts @@ -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; diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts new file mode 100644 index 000000000..930c51cf2 --- /dev/null +++ b/packages/compiler/src/backend/library-pack.ts @@ -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 { + 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 => { + 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 }); + } +} diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts index e2acecc0b..47bacfbb8 100644 --- a/packages/compiler/src/backend/linker.ts +++ b/packages/compiler/src/backend/linker.ts @@ -81,7 +81,7 @@ export async function executableLinkerEnvironmentFingerprint( defaultLinker: string = "clang", ): Promise { 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"], { diff --git a/packages/compiler/src/backend/literals.test.ts b/packages/compiler/src/backend/literals.test.ts index 799a70546..c2cbcddd0 100644 --- a/packages/compiler/src/backend/literals.test.ts +++ b/packages/compiler/src/backend/literals.test.ts @@ -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"); }); diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 66a08eb0f..23642cdf3 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index a5bc990c9..dcd3e543a 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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): Map { const metaMap = new Map(); 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( diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index d227433d1..53ddb401b 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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)`); diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts new file mode 100644 index 000000000..55d161caa --- /dev/null +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -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"); +}); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 617b64109..9d98289b4 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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(); /** 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(); /** Interned regex literals: "/" → { symbol, interned * source/flags literal refs } — one immortal ScrRegex per distinct @@ -271,18 +226,18 @@ export class LlEmitter { private readonly globalTypes = new Map(); private readonly tdzGlobals = new Map(); readonly constantNumericTables: ReadonlyMap; - /** 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; 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(); /** 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(); readonly resolveThunkDefs: string[] = []; /** ReadableStream.from adapters keep typed arrays by reference and box @@ -303,15 +258,16 @@ export class LlEmitter { readonly tracedShapes: Set; readonly tracedUnions: Set; /** The class graph (buildClassGraph): preorder numbering, hierarchy - * membership, virtual slot lists — the CEmitter classMeta, ported. */ + * membership, virtual slot lists. */ readonly classMeta: Map; /** 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(); + private readonly unionFieldReadGroups = new Map(); /** 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(); /** 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}`); - return; + 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"; } - this.releaseForJump(0, 0); - if (this.currentWasiCoro !== null) { - this.B.terminate(`br label %${this.currentWasiCoro.finalLabel}`); + // 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; } - 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"); + let cleanup = this.unwindCleanups.get(key); + if (!cleanup) { + cleanup = { label: this.B.newLabel("exc.cleanup"), entries, terminator }; + this.unwindCleanups.set(key, cleanup); + } + 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(); + 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); } diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 0d3acb2bc..70eedb34c 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index 8fa6efeaf..18b0e5a6c 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -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}`); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 5ee61a3e7..0456a25ac 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -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 }); diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index fe7102e02..6bb659d34 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index df5972cae..24ac56836 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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]); diff --git a/packages/compiler/src/backend/llvm/expr-island.ts b/packages/compiler/src/backend/llvm/expr-island.ts index 09371fd7f..a75e15f95 100644 --- a/packages/compiler/src/backend/llvm/expr-island.ts +++ b/packages/compiler/src/backend/llvm/expr-island.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 1aaeec16e..b26deecc7 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -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 }); diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 12460f1d0..0d282369b 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -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`); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 340eb9e02..7cd571f49 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts new file mode 100644 index 000000000..c02b1c211 --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-abi.ts @@ -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 }); +} diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts new file mode 100644 index 000000000..3ac717edf --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -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]!]); +} diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts new file mode 100644 index 000000000..6d71299cf --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -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 = { + "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}`); + } +} diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 2037f447c..08902f5c9 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -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; diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index f9ddd0955..af52316df 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts new file mode 100644 index 000000000..5bb94d7dc --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -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 = { + "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)]); +} diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts new file mode 100644 index 000000000..bf29eb9b1 --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -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)]); +} diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index fff618b16..51faadbc2 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -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})`); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 827c6f5a7..c95d5c045 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -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"); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index e41b76f85..cf7b923e6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -24,6 +24,9 @@ export function emitAlwaysThrowLibCall( } export const LIB_FN_SYMS: Record = { + "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", diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts new file mode 100644 index 000000000..0ef6fdd92 --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -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}`); + } +} diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts new file mode 100644 index 000000000..dc7fdd525 --- /dev/null +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -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}`); + } +} diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 28379492b..4a5c2bd1d 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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> = { f64: 0, date: 0, bool: 1, string: 2, func: 4 }; + const plain: Partial> = { 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. */ diff --git a/packages/compiler/src/backend/llvm/unsupported.ts b/packages/compiler/src/backend/llvm/unsupported.ts index 45b07d6c7..f10ce0a23 100644 --- a/packages/compiler/src/backend/llvm/unsupported.ts +++ b/packages/compiler/src/backend/llvm/unsupported.ts @@ -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"; } diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 1fe34fc0c..8aeb6da97 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -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. */ diff --git a/packages/compiler/src/backend/c/may-throw.test.ts b/packages/compiler/src/backend/may-throw.test.ts similarity index 98% rename from packages/compiler/src/backend/c/may-throw.test.ts rename to packages/compiler/src/backend/may-throw.test.ts index ea6060084..d8782e17b 100644 --- a/packages/compiler/src/backend/c/may-throw.test.ts +++ b/packages/compiler/src/backend/may-throw.test.ts @@ -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 }; diff --git a/packages/compiler/src/backend/c/may-throw.ts b/packages/compiler/src/backend/may-throw.ts similarity index 98% rename from packages/compiler/src/backend/c/may-throw.ts rename to packages/compiler/src/backend/may-throw.ts index 9f15dfe0b..4ea8dd6f8 100644 --- a/packages/compiler/src/backend/c/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -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 diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index ed31f9177..9cd5bb829 100644 --- a/packages/compiler/src/backend/native-codegen.test.ts +++ b/packages/compiler/src/backend/native-codegen.test.ts @@ -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"); }); diff --git a/packages/compiler/src/backend/native-codegen.ts b/packages/compiler/src/backend/native-codegen.ts index 026de36b0..3d55200a0 100644 --- a/packages/compiler/src/backend/native-codegen.ts +++ b/packages/compiler/src/backend/native-codegen.ts @@ -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 { 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 => { let stdout: string; let binary: Buffer; diff --git a/packages/compiler/src/backend/native-link-info.test.ts b/packages/compiler/src/backend/native-link-info.test.ts index 936507d87..28911d58a 100644 --- a/packages/compiler/src/backend/native-link-info.test.ts +++ b/packages/compiler/src/backend/native-link-info.test.ts @@ -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 }); - 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 () => { +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: { - ...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, + 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.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 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("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("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.system_libraries).toEqual(["sqlite3", "System"]); + 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(); }); }); diff --git a/packages/compiler/src/backend/native-link-info.ts b/packages/compiler/src/backend/native-link-info.ts index ba5349c6a..9bb52201b 100644 --- a/packages/compiler/src/backend/native-link-info.ts +++ b/packages/compiler/src/backend/native-link-info.ts @@ -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(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 { 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 ?? [])], }, }; diff --git a/packages/compiler/src/backend/native-toolchain.test.ts b/packages/compiler/src/backend/native-toolchain.test.ts index e4100276f..a9862007e 100644 --- a/packages/compiler/src/backend/native-toolchain.test.ts +++ b/packages/compiler/src/backend/native-toolchain.test.ts @@ -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, diff --git a/packages/compiler/src/backend/native-toolchain.ts b/packages/compiler/src/backend/native-toolchain.ts index 576404c5a..05c81d505 100644 --- a/packages/compiler/src/backend/native-toolchain.ts +++ b/packages/compiler/src/backend/native-toolchain.ts @@ -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 (.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 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 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 * 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 { - 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//.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, diff --git a/packages/compiler/src/backend/object-localize.test.ts b/packages/compiler/src/backend/object-localize.test.ts index 94c93bc99..370451918 100644 --- a/packages/compiler/src/backend/object-localize.test.ts +++ b/packages/compiler/src/backend/object-localize.test.ts @@ -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, diff --git a/packages/compiler/src/backend/object-localize.ts b/packages/compiler/src/backend/object-localize.ts index 783dba78e..5b79448df 100644 --- a/packages/compiler/src/backend/object-localize.ts +++ b/packages/compiler/src/backend/object-localize.ts @@ -652,8 +652,13 @@ export function mergeAndLocalizeCoffObjects( fail(`${object.label}: COMDAT section ${section.name} has unsupported selection ${section.comdatSelection}`); } const leader = section.comdatLeader >= 0 ? object.symbols[section.comdatLeader] : undefined; - if (leader === undefined || leader.storageClass !== IMAGE_SYM_CLASS_EXTERNAL) { - fail(`${object.label}: COMDAT section ${section.name} has no external leader symbol`); + // Function-section builds also mark private code and unwind tables + // as COMDAT. They have a static leader or just a section symbol. + // Their names are local to the input object, so preserve each copy + // and its relocations instead of deduplicating across objects. + if (leader === undefined || leader.storageClass === IMAGE_SYM_CLASS_STATIC) return; + if (leader.storageClass !== IMAGE_SYM_CLASS_EXTERNAL) { + fail(`${object.label}: COMDAT section ${section.name} has an invalid leader symbol`); } const existing = comdatKept.get(leader.name); if (existing === undefined) { diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 4c8337a62..c3c28742e 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -35,6 +35,14 @@ interface RuntimePackArchive extends RuntimePackArtifact { predicate: RuntimePredicate; } +export type RuntimePackMode = "executable" | "library" | "library-thread"; +export type RuntimePackFlavor = "release" | "dev" | "library-release" | "library-dev" | "library-thread-release" | "library-thread-dev"; + +interface RuntimePackFlavorManifest { + optimization: "-O2" | "-O0"; + runtime_units: RuntimePackUnit[]; +} + export interface RuntimePackManifest { schema: typeof RUNTIME_PACK_SCHEMA; format: typeof RUNTIME_PACK_FORMAT; @@ -54,10 +62,7 @@ export interface RuntimePackManifest { excluded: string[]; sanitizer: "external-toolchain-required"; }; - flavors: Record<"release" | "dev", { - optimization: "-O2" | "-O0"; - runtime_units: RuntimePackUnit[]; - }>; + flavors: Partial>; archives: RuntimePackArchive[]; system_libraries: { name: string; predicate: RuntimePredicate }[]; licenses: { path: string; license: string }[]; @@ -140,7 +145,10 @@ export function parseRuntimePackManifest(value: unknown): RuntimePackManifest { !Array.isArray(macros?.executable) || !macros.executable.every((entry) => typeof entry === "string") || !Array.isArray(macros.excluded) || !macros.excluded.every((entry) => typeof entry === "string") || macros.sanitizer !== "external-toolchain-required" || - flavors === null || !validFlavor(flavors.release, "-O2") || !validFlavor(flavors.dev, "-O0") || + flavors === null || Object.keys(flavors).length === 0 || + Object.keys(flavors).some((name) => !["release", "dev", "library-release", "library-dev", "library-thread-release", "library-thread-dev"].includes(name)) || + ((flavors.release !== undefined || flavors.dev !== undefined) && (!validFlavor(flavors.release, "-O2") || !validFlavor(flavors.dev, "-O0"))) || + Object.entries(flavors).some(([name, flavor]) => !validFlavor(flavor, name.endsWith("dev") ? "-O0" : "-O2")) || !Array.isArray(manifest.archives) || !manifest.archives.every((raw) => { const archive = object(raw); return validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate); @@ -257,11 +265,20 @@ export function selectRuntimePackArtifacts( requested: NativeLinkFeatures, flavor: "release" | "dev", env: NodeJS.ProcessEnv = process.env, + mode: RuntimePackMode = "executable", ): RuntimePackArtifacts { const features = effectiveRuntimeFeatures(requested, env); + const key: RuntimePackFlavor = mode === "executable" ? flavor : `${mode}-${flavor}`; + const selectedFlavor = mode === "library" + ? flavor === "release" ? manifest.flavors["library-release"] : manifest.flavors["library-dev"] + : mode === "library-thread" + ? flavor === "release" ? manifest.flavors["library-thread-release"] : manifest.flavors["library-thread-dev"] + : flavor === "release" ? manifest.flavors.release : manifest.flavors.dev; + if (selectedFlavor === undefined) throw new RuntimePackError(`runtime pack has no ${key} flavor; reinstall the matching runtime package`, "invalid"); + if (mode !== "executable" && requested.dynamic) throw new RuntimePackError("library runtime packs do not support dynamic execution", "unsupported"); return { features, - runtime: manifest.flavors[flavor].runtime_units + runtime: selectedFlavor.runtime_units .filter((unit) => evaluateRuntimePredicate(unit.predicate, features)) .map((unit) => selectVariant(unit, features)), archives: manifest.archives.filter((archive) => evaluateRuntimePredicate(archive.predicate, features)), diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 5a704e9bd..f2d3b9ed0 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -536,3 +536,27 @@ describe("runtime pack manifests", () => { }, ); }); + +test("library runtime selection requires dedicated packs and never substitutes executable objects", async () => { + const f = await fixture(); + const options = { target: MACOS_ARM64_TARGET, features: BASE, optimization: "release" as const, resolver: () => f.packagePath }; + await expect(loadRuntimePack({ ...options, mode: "library" })).rejects.toThrow("no library-release flavor"); + await expect(loadRuntimePack({ ...options, mode: "library-thread" })).rejects.toThrow("no library-thread-release flavor"); + const unit = f.manifest.flavors.release!.runtime_units[0]!; + const variant = unit.variants[0]!; + for (const [name, defines] of [ + ["library-release", ["SCR_LIB"]], + ["library-thread-release", ["SCR_LIB", "SCR_THREAD_INSTANCES"]], + ] as const) { + f.manifest.flavors[name] = { optimization: "-O2", runtime_units: [{ ...unit, variants: [{ ...variant, defines: [...defines] }] }] }; + } + await writeFile(join(f.root, "runtime-pack.json"), JSON.stringify(f.manifest)); + const threaded = await loadRuntimePack({ ...options, mode: "library-thread" }); + expect(threaded.runtimeObjects.map((path) => basename(path))).toEqual(["base.o"]); + await expect(loadRuntimePack({ ...options, features: { ...BASE, dynamic: true }, mode: "library" })).rejects.toThrow("do not support dynamic"); + delete f.manifest.flavors.release; + delete f.manifest.flavors.dev; + await writeFile(join(f.root, "runtime-pack.json"), JSON.stringify(f.manifest)); + await expect(loadRuntimePack({ ...options, mode: "library-thread" })).resolves.toMatchObject({ runtimeObjects: threaded.runtimeObjects }); + await expect(loadRuntimePack(options)).rejects.toThrow("no release flavor"); +}); diff --git a/packages/compiler/src/backend/runtime-pack.ts b/packages/compiler/src/backend/runtime-pack.ts index e5e543bbf..8fb6bea6d 100644 --- a/packages/compiler/src/backend/runtime-pack.ts +++ b/packages/compiler/src/backend/runtime-pack.ts @@ -13,7 +13,7 @@ import type { NativeTargetSpec } from "./targets.js"; import { RuntimePackError, parseRuntimePackManifest, selectRuntimePackArtifacts, validateRuntimePackIdentity, - type RuntimePackArtifact, type RuntimePackManifest, type RuntimeFeatureSet, + type RuntimePackArtifact, type RuntimePackManifest, type RuntimeFeatureSet, type RuntimePackMode, } from "./runtime-pack-core.js"; export { RUNTIME_PACK_SCHEMA, RUNTIME_PACK_FORMAT, RuntimePackError, parseRuntimePackManifest, @@ -123,6 +123,7 @@ export async function loadRuntimePack(options: { target: NativeTargetSpec; features: NativeLinkFeatures; optimization: "release" | "dev"; + mode?: RuntimePackMode; env?: NodeJS.ProcessEnv; resolver?: (specifier: string) => string; }): Promise { @@ -158,7 +159,7 @@ export async function loadRuntimePack(options: { } validateRuntimePackIdentity(manifest, packageManifest.name, packageManifest.version, options.target, compilerReleaseVersion()); const flavor = options.optimization; - const selected = selectRuntimePackArtifacts(manifest, options.features, flavor, options.env); + const selected = selectRuntimePackArtifacts(manifest, options.features, flavor, options.env, options.mode); const features = selected.features; const selectedVariants = selected.runtime; const selectedArchives = selected.archives; diff --git a/packages/compiler/src/backend/targets.test.ts b/packages/compiler/src/backend/targets.test.ts index 32679dbd2..1fe162e75 100644 --- a/packages/compiler/src/backend/targets.test.ts +++ b/packages/compiler/src/backend/targets.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "vitest"; import { + IOS_ARM64_TARGET, + IOS_SIMULATOR_ARM64_TARGET, + ANDROID_ARM64_TARGET, + nativeHelperForTarget, LINUX_ARM64_GNU_TARGET, LINUX_ARM64_MUSL_TARGET, LINUX_X64_GNU_TARGET, @@ -51,7 +55,7 @@ describe("native code-generation targets", () => { expect(nativeCodegenTarget( { SCRIPTC_TARGET: "aarch64-apple-ios" }, "darwin", "arm64", "24.0.0", )) - .toBeNull(); + .toEqual(IOS_ARM64_TARGET); }); test("refusals name the unsupported host or cross target", () => { @@ -59,11 +63,23 @@ describe("native code-generation targets", () => { expect(nativeCodegenTargetRefusal({}, "darwin", "arm64", "23.6.0")) .toContain("requires macOS 15.0 or newer"); expect(nativeCodegenTargetRefusal( - { SCRIPTC_TARGET: "x86_64-linux-gnu.2.36" }, + { SCRIPTC_TARGET: "riscv64-linux-gnu" }, "darwin", "arm64", "24.0.0", - )).toContain("SCRIPTC_TARGET=x86_64-linux-gnu.2.36"); + )).toContain("SCRIPTC_TARGET=riscv64-linux-gnu"); + }); + + test("cross targets select the host helper and a target linker", () => { + const cross = nativeCodegenTarget({ SCRIPTC_TARGET: "x86_64-linux-gnu.2.36" }, "darwin", "arm64", "24.0.0")!; + expect(cross).toMatchObject({ name: "linux-x64-gnu", defaultLinker: "zig", linkerTargetTriple: "x86_64-linux-gnu.2.36" }); + expect(nativeHelperForTarget(cross, "darwin", "arm64")?.packageName).toBe("@scriptc/llvm-darwin-arm64"); + expect(nativeHelperForTarget(MACOS_ARM64_TARGET, "win32", "x64")?.packageName).toBe("@scriptc/llvm-win32-x64-msvc"); + expect(nativeCodegenTarget({ SCRIPTC_TARGET: "aarch64-linux-android" }, "win32", "x64", "10.0.0")).toEqual(ANDROID_ARM64_TARGET); + expect(nativeCodegenTarget({ SCRIPTC_TARGET: "aarch64-apple-ios-simulator" }, "darwin", "arm64", "24.0.0")).toEqual(IOS_SIMULATOR_ARM64_TARGET); + for (const target of [IOS_ARM64_TARGET, IOS_SIMULATOR_ARM64_TARGET, ANDROID_ARM64_TARGET]) { + expect(target.supports).toEqual({ asm: true, obj: true, exe: false, library: true }); + } }); test("owns helper executable linker arguments in the target specification", () => { diff --git a/packages/compiler/src/backend/targets.ts b/packages/compiler/src/backend/targets.ts index 0964294a8..1824f986f 100644 --- a/packages/compiler/src/backend/targets.ts +++ b/packages/compiler/src/backend/targets.ts @@ -1,7 +1,6 @@ import { release } from "node:os"; -/** A helper package is host-specific, even when it emits a portable target - * (the WASI helpers are the first intentionally cross-host case). */ +/** Helpers run on the build host and emit every supported target ABI. */ export interface NativeHelperSpec { packageName: string; /** The helper protocol reports this primary target and its layout. */ @@ -18,7 +17,10 @@ export type NativeTargetName = | "windows-x64-msvc" | "linux-x64-musl" | "linux-arm64-musl" - | "wasm32-wasi"; + | "wasm32-wasi" + | "ios-arm64" + | "ios-simulator-arm64" + | "android-arm64"; export type NativeObjectFormat = "macho" | "elf" | "coff" | "wasm"; export type NativeTargetPlatform = "darwin" | "linux" | "win32" | "wasi"; @@ -191,7 +193,7 @@ export const MACOS_ARM64_TARGET: NativeTargetSpec = { defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["System"], linkerTargetTriple: "arm64-apple-macosx14.0.0", runtimeCompileDefines: [], - supports: { asm: true, obj: true, exe: true, library: false }, + supports: { asm: true, obj: true, exe: true, library: true }, helperPackage: DARWIN_ARM64_HELPER.packageName, helper: DARWIN_ARM64_HELPER, llvmBackend: "AArch64", runtimePackPackage: "@scriptc/runtime-darwin-arm64", @@ -206,7 +208,7 @@ export const MACOS_X64_TARGET: NativeTargetSpec = { defaultLinker: "clang", defaultLinkerArgs: [], runtimeSystemLibraries: ["System"], linkerTargetTriple: "x86_64-apple-macosx14.0.0", runtimeCompileDefines: [], - supports: { asm: true, obj: true, exe: true, library: false }, + supports: { asm: true, obj: true, exe: true, library: true }, helperPackage: DARWIN_X64_HELPER.packageName, helper: DARWIN_X64_HELPER, llvmBackend: "X86", runtimePackPackage: "@scriptc/runtime-darwin-x64", @@ -307,10 +309,35 @@ export const WASM32_WASI_TARGET: NativeTargetSpec = { runtimePackPackage: "@scriptc/runtime-wasm32-wasi", }; +export const IOS_ARM64_TARGET: NativeTargetSpec = { + ...MACOS_ARM64_TARGET, + name: "ios-arm64", llvmTriple: "arm64-apple-ios15.0.0", + minimumOs: "15.0", linkerTargetTriple: "aarch64-ios.15.0", + supports: { asm: true, obj: true, exe: false, library: true }, + runtimePackPackage: "@scriptc/runtime-ios-arm64", +}; + +export const IOS_SIMULATOR_ARM64_TARGET: NativeTargetSpec = { + ...IOS_ARM64_TARGET, + name: "ios-simulator-arm64", llvmTriple: "arm64-apple-ios15.0.0-simulator", + linkerTargetTriple: "aarch64-ios.15.0-simulator", + runtimePackPackage: "@scriptc/runtime-ios-simulator-arm64", +}; + +export const ANDROID_ARM64_TARGET: NativeTargetSpec = { + ...LINUX_ARM64_GNU_TARGET, + name: "android-arm64", llvmTriple: "aarch64-unknown-linux-android26", + minimumOs: "Android 26", defaultLinker: "zig", defaultLinkerArgs: ["cc"], + linkerTargetTriple: "aarch64-linux-android.26", + supports: { asm: true, obj: true, exe: false, library: true }, + runtimePackPackage: "@scriptc/runtime-android-arm64", +}; + export const NATIVE_TARGETS = [ MACOS_ARM64_TARGET, MACOS_X64_TARGET, LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET, WINDOWS_X64_MSVC_TARGET, LINUX_X64_MUSL_TARGET, LINUX_ARM64_MUSL_TARGET, WASM32_WASI_TARGET, + IOS_ARM64_TARGET, IOS_SIMULATOR_ARM64_TARGET, ANDROID_ARM64_TARGET, ] as const; function detectedLinuxLibc(): LinuxLibc { @@ -343,9 +370,15 @@ export function nativeHelperForTarget( ): NativeHelperSpec | null { const host = helperHost(hostPlatform, hostArch, linuxLibc); if (host === null) return null; - return target.hostHelpers?.[host] ?? ( - target.platform === hostPlatform && target.architecture === hostArch ? target.helper : null - ); + switch (host) { + case "darwin-arm64": return DARWIN_ARM64_HELPER; + case "darwin-x64": return DARWIN_X64_HELPER; + case "linux-x64-gnu": return LINUX_X64_HELPER; + case "linux-arm64-gnu": return LINUX_ARM64_HELPER; + case "linux-x64-musl": return LINUX_X64_MUSL_HELPER; + case "linux-arm64-musl": return LINUX_ARM64_MUSL_HELPER; + case "win32-x64": return WINDOWS_X64_HELPER; + } } function nativeHostTarget( @@ -373,30 +406,31 @@ function requestedTarget( raw: string, host: NativeTargetSpec | null, hostPlatform: NodeJS.Platform, - hostArch: string, - linuxLibc: LinuxLibc, ): NativeTargetSpec | null { if (raw === "") return host; switch (raw) { - case "arm64-apple-macosx14.0.0": return host?.name === "macos-arm64" ? MACOS_ARM64_TARGET : null; + case "arm64-apple-macosx14.0.0": + case "aarch64-macos": return MACOS_ARM64_TARGET; case "x86_64-apple-macosx14.0.0": - case "x86_64-apple-macos": return host?.name === "macos-x64" ? MACOS_X64_TARGET : null; + case "x86_64-apple-macos": + case "x86_64-macos": return MACOS_X64_TARGET; case "x86_64-unknown-linux-gnu": case "x86_64-linux-gnu": - case "x86_64-linux-gnu.2.36": return host?.name === "linux-x64-gnu" ? LINUX_X64_GNU_TARGET : null; + case "x86_64-linux-gnu.2.36": return LINUX_X64_GNU_TARGET; case "aarch64-unknown-linux-gnu": case "aarch64-linux-gnu": - case "aarch64-linux-gnu.2.36": return host?.name === "linux-arm64-gnu" ? LINUX_ARM64_GNU_TARGET : null; + case "aarch64-linux-gnu.2.36": return LINUX_ARM64_GNU_TARGET; case "x86_64-unknown-linux-musl": - case "x86_64-linux-musl": - return host?.platform === "linux" && host.architecture === "x64" ? LINUX_X64_MUSL_TARGET : null; + case "x86_64-linux-musl": return LINUX_X64_MUSL_TARGET; case "aarch64-unknown-linux-musl": - case "aarch64-linux-musl": - return host?.platform === "linux" && host.architecture === "arm64" ? LINUX_ARM64_MUSL_TARGET : null; + case "aarch64-linux-musl": return LINUX_ARM64_MUSL_TARGET; case "wasm32-wasi": - case "wasm32-unknown-wasi": - return helperHost(hostPlatform, hostArch, linuxLibc) === null ? null : WASM32_WASI_TARGET; - case "x86_64-pc-windows-msvc": return host?.name === "windows-x64-msvc" ? WINDOWS_X64_MSVC_TARGET : null; + case "wasm32-unknown-wasi": return WASM32_WASI_TARGET; + case "x86_64-windows-gnu": + case "x86_64-pc-windows-msvc": return WINDOWS_X64_MSVC_TARGET; + case "aarch64-apple-ios": return hostPlatform === "darwin" ? IOS_ARM64_TARGET : null; + case "aarch64-apple-ios-simulator": return hostPlatform === "darwin" ? IOS_SIMULATOR_ARM64_TARGET : null; + case "aarch64-linux-android": return ANDROID_ARM64_TARGET; default: return null; } } @@ -411,8 +445,20 @@ export function nativeCodegenTarget( linuxLibc: LinuxLibc = detectedLinuxLibc(), ): NativeTargetSpec | null { const host = nativeHostTarget(hostPlatform, hostArch, hostRelease, linuxLibc); - const target = requestedTarget(env["SCRIPTC_TARGET"] ?? "", host, hostPlatform, hostArch, linuxLibc); - return target !== null && nativeHelperForTarget(target, hostPlatform, hostArch, linuxLibc) !== null ? target : null; + const target = requestedTarget(env["SCRIPTC_TARGET"] ?? "", host, hostPlatform); + if (host === null || target === null || nativeHelperForTarget(target, hostPlatform, hostArch, linuxLibc) === null) return null; + // Cross ELF links use Zig's target libc. A native clang driver cannot + // infer or provide another architecture's CRT and sysroot. + if (target.platform === "linux" && target.name !== host.name && target.name !== "android-arm64") { + return { + ...target, defaultLinker: "zig", defaultLinkerArgs: ["cc"], + linkerTargetTriple: `${target.architecture === "x64" ? "x86_64" : "aarch64"}-linux-${target.name.endsWith("musl") ? "musl" : "gnu.2.36"}`, + }; + } + if (target.platform === "darwin" && hostPlatform !== "darwin") { + return { ...target, defaultLinker: "zig", defaultLinkerArgs: ["cc"], linkerTargetTriple: `${target.architecture === "x64" ? "x86_64" : "aarch64"}-macos.14.0` }; + } + return target; } export function nativeCodegenTargetRefusal( diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts new file mode 100644 index 000000000..5e01dfb20 --- /dev/null +++ b/packages/compiler/src/backend/warm-cache.ts @@ -0,0 +1,35 @@ +import { buildCacheRoot, prepareBuildCacheRoot } from "./build-cache.js"; +import { loadRuntimePack } from "./runtime-pack.js"; +import { nativeCodegenTarget, nativeCodegenTargetRefusal } from "./targets.js"; +import { warmNativeCaches as warmDevelopmentRuntime, type NativeCacheWarmProfile, type WarmNativeCachesOptions, type WarmNativeCachesResult } from "./native-toolchain.js"; + +/** Verify and read the installed runtime families before the first build. */ +export async function warmNativeCaches(options: WarmNativeCachesOptions = {}): Promise { + if (options.sanitize || process.env["SCRIPTC_FETCH_CURL"] === "1") return warmDevelopmentRuntime(options); + const cacheRoot = await prepareBuildCacheRoot(buildCacheRoot()); + if (cacheRoot === null) throw new Error("the native build cache is disabled or unavailable"); + const known = new Set(["runtime", "tls", "dynamic"]); + for (const profile of options.profiles ?? []) if (!known.has(profile)) throw new Error(`unknown native cache warm profile '${profile}'`); + const target = nativeCodegenTarget(); + if (target === null) throw new Error(nativeCodegenTargetRefusal() ?? "unsupported runtime-pack target"); + if (!target.supports.exe) throw new Error(`native cache warming requires an executable target; ${target.name} supports library archives`); + const profiles = [...new Set(options.profiles ?? ["runtime", "tls", "dynamic"] as const)]; + const results = []; + for (const profile of profiles) { + const started = performance.now(); + await loadRuntimePack({ + target, optimization: options.optimization ?? "release", + features: { + dynamic: profile === "dynamic", fetch: profile === "tls", regex: false, + copying: false, textDecoderLegacy: false, fileHandle: 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, + }, + }); + results.push({ profile, elapsedMs: Math.round((performance.now() - started) * 10) / 10 }); + } + return { cacheRoot, profiles: results }; +} diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index d2384f1f2..f097ae238 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -58,24 +58,24 @@ async function fixture(): Promise<{ options: EarlyExecutableCacheOptions; source: string; missing: string; - cPath: string; + llvmPath: string; irPath: string; }> { const dir = await mkdtemp(join(tmpdir(), "scriptc-early-executable-")); scratch.push(dir); const source = join(dir, "entry.ts"); - const cPath = join(dir, "entry.ll"); + const llvmPath = join(dir, "entry.ll"); const irPath = join(dir, "entry.ir.json"); await Promise.all([ writeFile(source, 'console.log("hello");\n'), - writeFile(cPath, "; generated llvm\n"), + writeFile(llvmPath, "; generated llvm\n"), writeFile(irPath, '{"irVersion":6}\n'), ]); return { root: join(dir, "cache"), source, missing: join(dir, "missing.ts"), - cPath, + llvmPath, irPath, options: { entryPath: source, @@ -84,7 +84,7 @@ async function fixture(): Promise<{ emitIr: true, sanitize: false, dynamic: false, - backend: "auto", + backend: "llvm", npmStatic: null, ffiProfile: null, target: "test", @@ -105,22 +105,22 @@ test("early executable cache restores the emitted TU, IR, and native feature gat trackedFileExists(f.missing); }); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: false, frontend: tracker.snapshot(), }); - await Promise.all([rm(f.cPath), rm(f.irPath)]); + await Promise.all([rm(f.llvmPath), rm(f.irPath)]); const alternateC = join(f.options.outDir, "entry.c"); - await writeFile(alternateC, "/* saved C backend */\n"); + await writeFile(alternateC, "/* user-owned native source */\n"); const hit = await readEarlyExecutableCache(f.root, f.options); expect(hit?.native).toEqual(native); expect(hit?.executableRestored).toBe(false); - expect(await readFile(f.cPath, "utf8")).toBe("; generated llvm\n"); + expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n"); expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); - expect(await readFile(alternateC, "utf8")).toBe("/* saved C backend */\n"); + expect(await readFile(alternateC, "utf8")).toBe("/* user-owned native source */\n"); }); test("early dev cache restores the matching dSYM and rejects a missing payload", async () => { @@ -133,7 +133,7 @@ test("early dev cache restores the matching dSYM and rejects a missing payload", const symbols = Buffer.concat([Buffer.from("SCDSYM01"), Buffer.from([5, 0, 0, 0]), Buffer.from("plistDWARF")]); await installDarwinDebugSymbols(symbols, f.options.outPath); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, irPath: f.irPath, native: { ...native, optimization: "dev" }, + llvmPath: f.llvmPath, irPath: f.irPath, native: { ...native, optimization: "dev" }, executableRestored: true, nativeDependencies: [], frontend: tracker.snapshot(), }); await rm(`${f.options.outPath}.dSYM`, { recursive: true }); @@ -154,7 +154,7 @@ test("early executable cache misses on source and resolution changes", async () trackedFileExists(f.missing); }); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: false, @@ -174,7 +174,7 @@ test("early executable keys isolate compile modes, paths, implementation, and FF const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: false, @@ -184,7 +184,6 @@ test("early executable keys isolate compile modes, paths, implementation, and FF { ...f.options, emitIr: false }, { ...f.options, sanitize: true }, { ...f.options, dynamic: true }, - { ...f.options, backend: "c" }, { ...f.options, optimization: "dev" }, { ...f.options, npmStatic: "auto" }, { ...f.options, npmStatic: ["commander"] }, @@ -206,7 +205,7 @@ test("early executable cache rejects corruption and refreshes payload LRU times" const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: false, @@ -239,7 +238,7 @@ test("early executable cache restores a validated final binary", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: true, @@ -292,7 +291,7 @@ test("routed executable hits require an unchanged compiler implementation proof" const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: true, @@ -326,7 +325,7 @@ test("routed executable hits require an unchanged compiler implementation proof" // Publishing the same route again must replace its metadata on every // platform (notably Windows, whose rename does not overwrite files). await publishEarlyExecutableCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, native, executableRestored: true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 749f8a87f..6cb490c9b 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -26,10 +26,9 @@ interface CachedExecutableFile { } export interface EarlyExecutableNativeFeatures { - backend: "c" | "llvm"; + backend: "llvm"; /** Omitted is the historical release posture. */ optimization?: "dev"; - llvmRefusal?: string; dynamic: boolean; regex: boolean; copying: boolean; @@ -84,7 +83,7 @@ export interface EarlyExecutableCacheOptions { emitIr: boolean; sanitize: boolean; dynamic: boolean; - backend: "auto" | "c" | "llvm"; + backend: "llvm"; /** Omitted is the historical release posture and preserves v1 keys. */ optimization?: "dev"; /** Omitted retains the unstripped executable's historical cache key. */ @@ -123,7 +122,7 @@ interface EarlyExecutableImplementationProof { } export interface EarlyExecutableCacheHit { - cPath: string; + llvmPath: string; irPath?: string; /** True when the final executable was restored after native dependencies * validated. False restores only frontend artifacts and must call compileC. */ @@ -174,8 +173,7 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat value, BOOLEAN_NATIVE_KEYS, (native) => - (native.optimization === undefined || native.optimization === "dev") && - (native.llvmRefusal === undefined || typeof native.llvmRefusal === "string"), + (native.optimization === undefined || native.optimization === "dev"), ); } @@ -291,7 +289,7 @@ function stampIntegrity(stamp: Omit): st function executableFrontendOutputExclusions( options: EarlyExecutableCacheOptions, - backend: "c" | "llvm", + backend: "llvm", ): ReturnType { return frontendOutputExclusions(options, backend, "", [options.outPath, `${options.outPath}.dSYM`]); } @@ -402,8 +400,8 @@ export async function readEarlyExecutableCache( // update must not win the window immediately before installation. await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies); const paths = outputPaths(options, stamp.native.backend); - if (!(await fileMatches(paths.cPath, stamp.files.translationUnit.digest))) { - await installBytes(translationUnit, paths.cPath); + if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) { + await installBytes(translationUnit, paths.llvmPath); } if ( ir !== null && stamp.files.ir !== null && @@ -432,7 +430,7 @@ export async function readEarlyExecutableCache( ...(stamp.files.debugSymbols === undefined ? [] : [join(directory, stamp.files.debugSymbols.name)]), ].map((cachePath) => utimes(cachePath, now, now).catch(() => undefined))); return { - cPath: paths.cPath, + llvmPath: paths.llvmPath, native: stamp.native, executableRestored, frontend: stamp.frontend, @@ -565,7 +563,7 @@ export async function publishEarlyExecutableCache( ? Promise.resolve(null) : publishFile(options.outPath, "program.bin"); const [translationUnit, ir, executable] = await Promise.all([ - publishFile(result.cPath, "program.tu"), + publishFile(result.llvmPath, "program.tu"), result.irPath === undefined ? Promise.resolve(null) : publishFile(result.irPath, "program.ir.json"), diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts new file mode 100644 index 000000000..0bc9ac3c6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "vitest"; +import { DYN, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { ClassDynamicDispatch } from "./class-dynamic-dispatch.js"; +import type { Lowerer } from "./lowerer.js"; + +const loc = { file: "dispatch.ts", start: 0, end: 1 }; +const variable = (localId: string): IrExpr => ({ kind: "varRef", localId, type: DYN, loc }); +const literal = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc }); +const statement = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc }); +const fn = (name: string, body: IrStmt[]): IrFunction => ({ name, params: [], returnType: VOID, locals: [], body, loc }); +function context() { + const liftedFns: IrFunction[] = []; + const lowerer = { shapes: new Map(), unions: new Map(), classes: new Map(), liftedFns } as unknown as Lowerer; + const boxed = fn("box", [statement({ kind: "dynFrom", value: { kind: "varRef", localId: "instance", type: { kind: "object", className: "Widget" }, loc }, type: DYN, loc })]); + return { lowerer, liftedFns, boxed }; +} + +test("preserves typed function bodies when other functions box classes", () => { + const { lowerer, boxed } = context(); + const body: IrStmt[] = [{ kind: "block", body: [statement({ kind: "numLit", value: 1, type: F64, loc })], loc }]; + const typed = fn("typed", body); + const boxBody = boxed.body; + const dispatch = new ClassDynamicDispatch(); + expect(dispatch.process(lowerer, [boxed, typed])).toBe(true); + expect(typed.body).toBe(body); + expect(boxed.body).toBe(boxBody); + expect(dispatch.process(lowerer, [boxed, typed])).toBe(false); + expect(typed.body).toBe(body); +}); + +test.each(["named read", "computed read", "named write", "computed string write", "computed dynamic write"])("rewrites a nested %s", (kind) => { + const { lowerer, boxed, liftedFns } = context(); + const read = kind.endsWith("read"); + const computed = kind.startsWith("computed"); + const dynamicKey = kind === "computed dynamic write"; + const key = computed ? { kind: "varRef" as const, localId: "key", type: dynamicKey ? DYN : STRING, loc } : literal("name"); + const operation: IrExpr = read + ? { kind: "dynKeyGet", value: variable("receiver"), key, type: DYN, loc } + : { kind: "libCall", fn: dynamicKey ? "dyn.keySetComputed" : "dyn.keySet", args: [variable("receiver"), key, variable("stored")], type: VOID, loc }; + const target = fn("target", [{ kind: "block", body: [statement(operation)], loc }]); + const dispatch = new ClassDynamicDispatch(); + dispatch.process(lowerer, [boxed, target]); + const block = target.body[0]!; + if (block.kind !== "block" || block.body[0]?.kind !== "exprStmt") throw new Error("missing test body"); + expect(block.body[0].expr).toMatchObject({ kind: "call", callee: expect.stringMatching(/^%dyn\.class\.(property|computed)\./) }); + expect(liftedFns).toHaveLength(2); + const transformed = target.body; + expect(dispatch.process(lowerer, [boxed, target, ...liftedFns])).toBe(false); + expect(target.body).toBe(transformed); +}); + +test("discovers dispatch sites added during a later reachability pass", () => { + const { lowerer, boxed } = context(); + const target = fn("target", []); + const dispatch = new ClassDynamicDispatch(); + dispatch.process(lowerer, [boxed, target]); + target.body.push(statement({ kind: "dynKeyGet", value: variable("receiver"), key: literal("name"), type: DYN, loc })); + expect(dispatch.process(lowerer, [boxed, target])).toBe(true); + expect(target.body[0]).toMatchObject({ kind: "exprStmt", expr: { kind: "call" } }); +}); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 0c9fc06bd..8b5095149 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -35,6 +35,7 @@ export class ClassDynamicDispatch { process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { const seenTypes = new Set(); + const rewrite = new Set(); const discover = (type: IrType): void => { const key = typeKey(type); if (seenTypes.has(key)) return; @@ -52,7 +53,18 @@ export class ClassDynamicDispatch { for (const fn of functions) everyStmtList(fn.body, { stmt: () => true, expr: (expr) => { - if (expr.kind === "dynFrom") discover(expr.value.type); + switch (expr.kind) { + case "dynFrom": + discover(expr.value.type); + break; + case "dynKeyGet": + case "dynInvoke": + rewrite.add(fn); + break; + case "libCall": + if (expr.fn === "dyn.keySet" || expr.fn === "dyn.keySetComputed") rewrite.add(fn); + break; + } return true; }, }); @@ -104,7 +116,10 @@ export class ClassDynamicDispatch { return matching; }; for (const fn of functions) { - if (this.generated.has(fn)) continue; + // Discovery already visits every expression. Preserve bodies without + // dispatch sites instead of rebuilding their entire typed IR tree on + // every reachability pass. Recompute this set as new bodies appear. + if (this.generated.has(fn) || !rewrite.has(fn)) continue; fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => { diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 1603788a8..47745e0bc 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -520,7 +520,7 @@ export function lowerRegexMethodCall(lowerer: Lowerer, call: ts.CallExpression, if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; const receiver = lowerReceiver(); // Split's omitted or undefined limit is 2^32-1. Complete it here so - // both IR backends and the runtime have one required (regex, limit) + // the LLVM backend and runtime have one required (regex, limit) // shape; an optional-number value selects the default at runtime. const args = name === "split" ? [lowerer.lowerExprExpecting(arg0, { kind: "regex" }), lowerSplitLimitArg(lowerer, call.arguments[1], loc)] @@ -557,7 +557,7 @@ export function lowerRegexMethodCall(lowerer: Lowerer, call: ts.CallExpression, /** `s.slice(1, 4)` and friends → strIntrinsic. Null when this isn't an * ambient string method call (caller keeps its generic rejection). - * Complete position defaults and conversions here for both backends. */ + * Complete position defaults and conversions here before code generation. */ export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, dynReceiver?: () => IrExpr, diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.test.ts b/packages/compiler/src/frontend/lowering/lib-boundary.test.ts new file mode 100644 index 000000000..f1404842b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/lib-boundary.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from "vitest"; +import { DYN, F64, STRING, VOID, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { enforceLibBoundary } from "./lib-boundary.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; + +const loc = { file: "boundary.ts", start: 0, end: 1 }; +const variable = (type: IrType): IrExpr => ({ kind: "varRef", localId: "value", type, loc }); +function context() { + const diagnostics: ScrDiagnostic[] = []; + const lowerer = { + jsonSafe: (type: IrType) => type.kind === "string" || type.kind === "f64", + fmt: (type: IrType) => type.kind, + pushDiag: (diagnostic: ScrDiagnostic) => { diagnostics.push(diagnostic); }, + } as unknown as Lowerer; + return { lowerer, diagnostics }; +} + +test("coerces nested expressions in place and keeps repeat visits idempotent", () => { + const { lowerer, diagnostics } = context(); + const conversion: IrExpr = { kind: "strIntrinsic", method: "toUpperCase", receiver: variable(DYN), args: [], type: STRING, loc }; + const call: IrExpr = { kind: "callValue", callee: variable({ kind: "func", params: [STRING], ret: VOID }), args: [variable(DYN)], type: VOID, loc }; + const statements: IrStmt[] = [{ kind: "block", loc, body: [ + { kind: "exprStmt", expr: conversion, loc }, + { kind: "exprStmt", loc, expr: { kind: "seqExpr", type: F64, loc, + stmts: [{ kind: "exprStmt", expr: call, loc }], result: { kind: "numLit", value: 1, type: F64, loc }, + } }, + ] }]; + enforceLibBoundary(lowerer, statements); + expect(conversion.receiver).toMatchObject({ kind: "dynCheck", type: STRING }); + expect(call.args[0]).toMatchObject({ kind: "dynCheck", type: STRING }); + const before = JSON.stringify(statements); + enforceLibBoundary(lowerer, statements); + expect(JSON.stringify(statements)).toBe(before); + expect(diagnostics).toEqual([]); +}); + +test("keeps child-first diagnostics inside nested statements", () => { + const { lowerer, diagnostics } = context(); + const badCall: IrExpr = { kind: "callValue", callee: variable(DYN), args: [], type: VOID, loc }; + const statement: IrStmt = { kind: "exprStmt", loc, expr: { + kind: "strIntrinsic", method: "toUpperCase", receiver: badCall, args: [], type: STRING, loc, + } }; + expect(() => enforceLibBoundary(lowerer, statement)).toThrow(PoisonError); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toMatchObject({ code: "SC1100", loc }); + expect(diagnostics[0]!.message).toContain("calling 'dyn' values"); +}); diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.ts b/packages/compiler/src/frontend/lowering/lib-boundary.ts index 03e9c616a..b76986c1d 100644 --- a/packages/compiler/src/frontend/lowering/lib-boundary.ts +++ b/packages/compiler/src/frontend/lowering/lib-boundary.ts @@ -36,9 +36,10 @@ * The pass never rewrites a well-typed argument: typeEquals matches are * untouched, so byte-stability holds for every program that lowered * cleanly before. The validator stays the backstop for anything else. */ +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; -import { canAdaptDynFuncTo, canMarshalTypedFuncIntoIsland, DYN, DYN_HANDLE_KINDS, type IrExpr, type IrType, JSVAL, type SrcLoc, STRING, isDynTypedRefType, isUnitType, typeEquals } from "../../ir/ir.js"; +import { canAdaptDynFuncTo, canMarshalTypedFuncIntoIsland, DYN, DYN_HANDLE_KINDS, type IrExpr, type IrStmt, type IrType, JSVAL, type SrcLoc, STRING, isDynTypedRefType, isUnitType, typeEquals } from "../../ir/ir.js"; import { LIB_FN_SIGS, REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/validate.js"; import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; @@ -123,19 +124,21 @@ function fence(lowerer: Lowerer, code: "SC1090" | "SC1100", loc: SrcLoc, feature * signature-table argument slot. Idempotent: a wrapped argument matches its * slot on a revisit (nested statement lists are walked by their own * lowerStmts call first, then again inside the enclosing statement). */ -export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { - if (node === null || typeof node !== "object") return; - if (Array.isArray(node)) { - for (const item of node) enforceLibBoundary(lowerer, item); - return; - } - const rec = node as Record; - for (const key of Object.keys(rec)) { - if (key !== "loc") enforceLibBoundary(lowerer, rec[key]); - } - const kind = rec["kind"]; +export function enforceLibBoundary(lowerer: Lowerer, node: IrStmt | IrStmt[]): void { + const stmt = (value: IrStmt): boolean => everyStmtChild(value, expr, stmt); + const expr = (value: IrExpr): boolean => { + everyExprChild(value, expr, stmt); + enforceExprBoundary(lowerer, value); + return true; + }; + if (Array.isArray(node)) node.forEach((value) => { stmt(value); }); + else stmt(node); +} + +function enforceExprBoundary(lowerer: Lowerer, node: IrExpr): void { + const kind = node.kind; if (kind === "libCall") { - const e = rec as unknown as Extract; + const e = node; const sig = LIB_FN_SIGS[e.fn]; if (!sig) return; e.args.forEach((a, i) => { @@ -145,7 +148,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { return; } if (kind === "strIntrinsic") { - const e = rec as unknown as Extract; + const e = node; e.receiver = coerceSlot(lowerer, e.receiver, STRING, `the receiver of .${e.method}`); const sig = STR_INTRINSIC_SIGS[e.method]; if (!sig) return; @@ -156,7 +159,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { return; } if (kind === "regexIntrinsic") { - const e = rec as unknown as Extract; + const e = node; const sig = REGEX_INTRINSIC_SIGS[e.method]; if (!sig) return; e.receiver = coerceSlot(lowerer, e.receiver, sig.receiver, `the receiver of .${e.method}`); @@ -167,7 +170,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { return; } if (kind === "arrIntrinsic") { - const e = rec as unknown as Extract; + const e = node; if (e.receiver.type.kind === "dyn" || e.receiver.type.kind === "jsval" || isUnitType(e.receiver.type)) { // No element type exists to validate a dyn receiver against — the // honest answer is the operations-on-unknown fence. An island @@ -182,7 +185,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { return; } if (kind === "callValue") { - const e = rec as unknown as Extract; + const e = node; if (e.callee.type.kind === "dyn" || e.callee.type.kind === "jsval" || isUnitType(e.callee.type)) { fence(lowerer, "SC1100", e.loc, `calling '${lowerer.fmt(e.callee.type)}' values`); } @@ -210,7 +213,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { // values with an island representation marshal in, and dyn values (no // dyn→engine bridge that preserves handles/functions) fence with // jsvalIn's own message. - const e = rec as unknown as Extract; + const e = node; e.args.forEach((a, i) => { if (a.type.kind === "jsval") return; if (isUnitType(a.type)) { @@ -236,7 +239,7 @@ export function enforceLibBoundary(lowerer: Lowerer, node: unknown): void { // take the ordinary dynFrom crossing; island values have NO bridge // into the checked-dynamic tree (a jsval handle cannot ride the deep-copy), so they // fence — named, catchable at runtime in JS sources, never an ICE. - const e = rec as unknown as Extract; + const e = node; if (kind === "dynInvoke") { const inv = e as Extract; if (inv.recv.type.kind !== "dyn") { diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index 0b852d2b8..3b1272b5c 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -8567,7 +8567,7 @@ const NUMBER_STATIC_PREDICATES: Record = { }; /** The Number constants, baked as literals — non-finite ones included - * (numLits carry NaN and the infinities; both backends spell them). */ + * (numLits carry NaN and the infinities; the backend preserves them). */ const NUMBER_CONSTANTS: Record = { MAX_SAFE_INTEGER: 9007199254740991, MIN_SAFE_INTEGER: -9007199254740991, diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index e0b35a390..997dabe6c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -53,7 +53,7 @@ export { bodyReadsArguments }; export type ParamMode = "required" | "omittable" | "rest" | "dynRest" | "islandRest" | "arguments"; /** One parameter of a signature, as call sites and callee prologues see it. - * `type` is the ABI type — what the emitted C parameter carries: the + * `type` is the ABI type — what the emitted LLVM parameter carries: the * checker's `T | undefined` union for `x?: T`, a synthesized `T | undefined` * union for `x: T = e`, `T[]` for `...xs: T[]`, the plain declared type * otherwise. `bodyType` is present exactly for DEFAULTED program params: @@ -467,8 +467,10 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration, * coercion an ordinary argument gets (coerceInto against its shape, * DYN conversion in a dyn rest, element coercion in a typed rest). */ leading?: readonly IrExpr[],): IrExpr[] { - if (canCompleteRuntimeSpread(lowerer, shapes) && - argNodes.some((arg) => ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression))) { + // Conversion eligibility can traverse the whole recursive parameter + // graph. Ordinary calls and fixed tuples never need that analysis. + if (argNodes.some((arg) => ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression)) && + canCompleteRuntimeSpread(lowerer, shapes)) { // Runtime-length spreads determine the complete argument list before // any parameter default runs. Build it once in source order, then // extract the fixed native ABI slots; missing elements are undefined. @@ -3382,7 +3384,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr } // An inline function value at a RELEASE site can never match: // lifted lambdas always carry a captures list (even an empty one), - // so both backends mint a fresh closure per evaluation of the + // so the backend creates a fresh closure per evaluation of the // expression — the release argument is a pointer no registration // holds, a guaranteed runtime trap. Declared functions stay valid // here — their value is the interned immortal closure (captures @@ -5888,7 +5890,6 @@ function lowerOptionalStringNumber( shapes: readonly ParamShape[], argNodes: readonly ts.Expression[], ): boolean { - if (canCompleteRuntimeSpread(lowerer, shapes)) return false; const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest"); let position = 0; for (const arg of argNodes) { @@ -5898,7 +5899,9 @@ function lowerOptionalStringNumber( position += tuple.fields.length; continue; } - if (restAt < 0 || position < restAt || shapes[restAt]!.mode !== "rest") return true; + if (restAt < 0 || position < restAt || shapes[restAt]!.mode !== "rest") { + return !canCompleteRuntimeSpread(lowerer, shapes); + } } position++; } @@ -6963,7 +6966,7 @@ function loweredTemplateStrings( loc: SrcLoc, ): IrExpr { // Normal runtime construction keeps this array aligned with the current - // ScrArr layout in both backends. The old static-header IR node encoded the + // ScrArr layout in the backend. The old static-header IR node encoded the // pre-sparse layout in LLVM and crashed as soon as a tag read its strings. const type = arrayOf(STRING); const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}`; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 94758e554..aee47c25f 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3971,6 +3971,13 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou } export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction[] { + // Local class bodies are lowered when their expression captures its + // lexical environment. Type collection alone can discover a class in + // an owner skipped by the coverage remainder; it must not relower the + // constructor without that environment. + if (info.localClass && info.localClass.context === null) { + return [...info.localClass.bodies.values()]; + } const out: IrFunction[] = []; const className = info.def.name; // Generic-class INSTANTIATIONS (and mixin instantiations) are diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 5417ac908..08f793779 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -3253,6 +3253,16 @@ function lowerPromiseThenPresence( : null; if (promiseType === null) return null; let receiver = lowerer.lowerExpr(access.expression); + if (receiver.type.kind === "union" && access.questionDotToken) { + const arms = lowerer.unions.get(receiver.type.unionId)?.arms ?? []; + if (arms.every((arm) => arm.kind === "promise" || isUnitType(arm))) { + return nullishBranches(lowerer, receiver, receiver.type, arms, result.type, locOf(access), + () => result.type.kind === "bool" + ? { kind: "boolLit", value: false, type: BOOL, loc: result.loc } + : { kind: "strLit", value: "undefined", type: STRING, loc: result.loc }, + () => result); + } + } if (receiver.type.kind === "union") { const optional = lowerer.runtimeOptionalPropertyReceiver(access.expression, receiver, promiseType, access.name.text); if (optional !== null) receiver = optional; @@ -3262,6 +3272,29 @@ function lowerPromiseThenPresence( receiver = { kind: "call", callee: helper, args: [receiver], type: promiseType, loc: receiver.loc }; } } + if (receiver.type.kind === "dyn") { + // A specialized JS parameter can retain checked storage while JSDoc + // describes a promise. Probe its actual value, preserving nullish + // guards and own-property reads on ordinary thenable objects. + const loc = locOf(access); + const local = lowerer.declareHiddenLocal("%thenReceiver", DYN); + const stable = varRef(local.id, DYN, loc); + const member: IrExpr = { + kind: "dynKeyGet", value: stable, + key: { kind: "strLit", value: "then", type: STRING, loc }, + ...(access.questionDotToken ? { optional: true as const } : {}), type: DYN, loc, + }; + const other: IrExpr = result.type.kind === "bool" + ? { kind: "dynTest", test: "truthy", value: member, type: BOOL, loc } + : { kind: "libCall", fn: "dyn.typeof", args: [member], type: STRING, loc }; + return { + kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], + result: { + kind: "ternary", cond: { kind: "dynTest", test: "promise", value: stable, type: BOOL, loc }, + then: result, else_: other, type: result.type, loc, + }, type: result.type, loc, + }; + } if (receiver.type.kind !== "promise") return null; if (isSafeToDiscard(receiver)) return result; return { @@ -6649,249 +6682,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr ); } if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) { - if (op === ts.SyntaxKind.BarBarToken) { - const fallback = lowerAbsenceAwareOrChain(lowerer, expr); - if (fallback) return fallback; - } - // JS value semantics: `a && b` is `toBool(a) ? b : a` — the result is - // an operand value, so both operands must share one IR kind (which is - // also what tsc's type says, modulo literal-union collapsing that - // mapType already handles). Mixed kinds (`n && s`) stay rejected. - const left = lowerAbsenceProbe(lowerer, expr.left) ?? lowerer.lowerExpr(expr.left); - const right = lowerer.lowerExpr(expr.right); - // A LITERAL-unit left operand (a compile-time undefined/null — the - // capability-probe members: `process.features.inspector || - // !flag.startsWith('--inspect')` reads undefined in a compiled - // binary): statically falsy, so the operator folds JS-exactly — - // `unit || X` IS X, `unit && X` IS the unit (X's side effects never - // run in JS either; the lowered right is simply dropped). Literal - // units only — computed unit-typed values keep the fences below. - if (left.kind === "unitLit") { - return op === ts.SyntaxKind.BarBarToken ? right : left; - } - // JS objects and symbols are always truthy. In `ref || fallback` - // the fallback is therefore unreachable and the result is the - // already-evaluated left value — the browser-fallback idiom used by - // packages for `process.argv || []` and `process.env || {}`. The - // operand itself remains in the IR, preserving reads/calls that - // produce the reference; only the unreachable right lowering drops. - if (op === ts.SyntaxKind.BarBarToken && REF_TRUTHY_KINDS.has(left.type.kind)) { - return left; - } - if (left.type.kind === "dyn" || right.type.kind === "dyn") { - // A checked-dynamic operand (`fn.name || ''` — - // test/common's _mustCallInner): both sides live in the checked-dynamic tree and - // the deciding test is ToBoolean over the dyn kind - // (scr_dyn_truthy) — JS value semantics exactly, result dyn. The - // non-dyn side converts through the usual boundary; a value with - // no dyn representation keeps the fence. - const l = lowerer.coerceToExpected(left, DYN); - const r = lowerer.coerceToExpected(right, DYN); - if (l.type.kind !== "dyn" || r.type.kind !== "dyn") { - lowerer.unsupported("SC1100", expr, "logical operators on 'unknown' values"); - } - return { - kind: "logical", - op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left: l, - right: r, - type: DYN, loc, - }; - } - if (left.type.kind === "jsval" || right.type.kind === "jsval") { - return { - kind: "logical", - op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left: lowerer.jsvalIn(left, expr.left), - right: lowerer.jsvalIn(right, expr.right), - type: JSVAL, loc, - }; - } - if (left.type.kind === "union" || right.type.kind === "union") { - // JS value semantics over a union: the deciding test is the ARM - // value's ToBoolean (a per-union helper), and the result is the - // deciding operand. Supported when the checker's type of the whole - // expression maps to ONE union both operands coerce into (same - // union passes through; a plain arm wraps — `u || undefined`). - // Everything else (`u && flag`, whose value would need a wider - // re-tagged union) stays fenced — in CONDITION position those - // shapes lower through lowerCondition's bool descent instead. - let target = lowerer.mapTypeOf(lowerer.typeOf(expr)); - // A broad-JSDoc npm-static body can leave the CHECKER result `any` - // even after declaration-backed specialization has recovered both - // operands (`helpOption && args.find(...)` in commander). When the - // left consists only of always-truthy references and falsy units, - // the static result is exactly the right operand plus those units. - if ( - target === null && - lowerer.implicitParamTypes !== null && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && - (lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 && - left.type.kind === "union" - ) { - const leftArms = lowerer.unions.get(left.type.unionId)?.arms; - const rightArms = right.type.kind === "union" - ? lowerer.unions.get(right.type.unionId)?.arms - : [right.type]; - const rightIsStatic = rightArms?.every( - (arm) => arm.kind !== "dyn" && arm.kind !== "caught" && arm.kind !== "jsval" && arm.kind !== "void", - ) ?? false; - if ( - leftArms && rightArms && rightIsStatic && - leftArms.some(isUnitType) && - leftArms.every((arm) => isUnitType(arm) || REF_TRUTHY_KINDS.has(arm.kind)) - ) { - const byKey = new Map(); - for (const arm of [...rightArms, ...leftArms.filter(isUnitType)]) { - byKey.set(typeKey(arm), arm); - } - const arms = [...byKey.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); - if (arms.length > 1) { - const source = right.type.kind === "union" ? lowerer.unions.get(right.type.unionId) : undefined; - target = { kind: "union", unionId: source - ? lowerer.unions.transform(source, arms) - : lowerer.unions.intern(arms) }; - } - } - } - if (target?.kind === "union") { - // `&&` whose left does NOT fit the result union: the checker - // built that result by DROPPING left arms which are always - // truthy (`Option | undefined && string | undefined` answers - // `string | undefined`, not `Option | string | undefined`). - // Evaluate the left once, test it in its own union, and retag it - // only on the falsy path. Any stranded reference arms are - // unreachable there because every JS object is truthy. - if (op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && !typeEquals(left.type, target)) { - const def = lowerer.unions.get(left.type.unionId); - const trappable = new Set(); - def?.arms.forEach((arm, tag) => { - if (lowerer.armTag(target.unionId, arm) < 0 && REF_TRUTHY_KINDS.has(arm.kind)) { - trappable.add(tag); - } - }); - const retag = trappable.size === 0 - ? null - : lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc, trappable); - if (retag) { - lowerer.requireTruthyUnion(left.type.unionId, expr); - const stmts: IrStmt[] = []; - let stable: IrExpr = left; - if (!isSafeToRepeat(left)) { - const local = lowerer.declareHiddenLocal("%and", left.type); - stmts.push({ kind: "varDecl", localId: local.id, init: left, loc }); - stable = varRef(local.id, left.type, loc); - } - const result: IrExpr = { - kind: "ternary", - cond: lowerer.ensureBool(stable, expr.left), - then: lowerer.coerceInto(expr.right, right, target), - else_: { kind: "call", callee: retag, args: [stable], type: target, loc }, - type: target, - loc, - }; - return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: target, loc }; - } - } - // `||` whose left does NOT fit the result union: the checker built - // that result by DROPPING the left's falsy arms (`process.env.X || - // null` is `string | null`, `|| 3000` is `string | number` — the - // `undefined` is gone from both), so coercing the left eagerly, as - // the shared shape below does, retags an arm the test is about to - // rule out and throws where Node yields the default. Single-eval - // instead: test the left in its OWN union and retag only on the - // truthy side, where the dropped arms are unreachable. - if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) { - const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc); - if (retag) { - lowerer.requireTruthyUnion(left.type.unionId, expr); - return { - kind: "orDefault", - left, - right: lowerer.coerceInto(expr.right, right, target), - retag, - type: target, - loc, - }; - } - } - lowerer.requireTruthyUnion(target.unionId, expr); - return { - kind: "logical", - op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left: lowerer.coerceInto(expr.left, left, target), - right: lowerer.coerceInto(expr.right, right, target), - type: target, loc, - }; - } - // `u || d` NARROWED by the default: the checker types the result - // as u's single non-unit arm (`marker() || "default"`) — evaluate - // u once, truthy extracts the arm, falsy takes d lazily (the - // orDefault node, nullish's truthiness sibling). An UNMAPPABLE - // checker result takes the same rule (`options.runner || - // defaultRunner` — tsc's union of two structurally-compatible - // function types has no representation, while the left's one - // non-unit arm is the only representable answer): the default - // must coerce into the arm or fence on its own. - if ( - op === ts.SyntaxKind.BarBarToken && - left.type.kind === "union" - ) { - const def = lowerer.unions.get(left.type.unionId); - const rest = def ? def.arms.filter((a) => !isUnitType(a)) : []; - const funcArmDefault = - rest[0]?.kind === "func" && (target === null || target.kind === "func"); - if (rest.length === 1 && ((target !== null && typeEquals(target, rest[0]!)) || funcArmDefault)) { - lowerer.requireTruthyUnion(left.type.unionId, expr); - const dflt = lowerer.lowerExprExpecting(expr.right, rest[0]!); - return { kind: "orDefault", left, right: dflt, type: rest[0]!, loc }; - } - } - // JavaScript defaults can join differently inferred object layouts. - // When both operands have native checked representations, preserve - // the deciding value and short-circuit evaluation in that domain. - if (isJsSourceFile(expr.getSourceFile()) && - lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { - return { - kind: "logical", op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left: lowerer.coerceInto(expr.left, left, DYN), - right: lowerer.coerceInto(expr.right, right, DYN), type: DYN, loc, - }; - } - lowerer.unsupported( - "SC1090", - expr, - `logical operators on union-typed values outside conditions (${NARROW_FIRST})`, - ); - } - const kind = left.type.kind; - if ( - kind !== right.type.kind || - (kind !== "f64" && kind !== "string" && kind !== "bool") - ) { - // Mixed PLAIN operands (`value || null`, `flag || undefined`, - // `s || 0`): JS value semantics still compile when the checker - // types the RESULT as one union both operands coerce into — the - // same lift the union-operand path above takes, arriving here - // with two plain arm values instead. - const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (target?.kind === "union") { - lowerer.requireTruthyUnion(target.unionId, expr); - return { - kind: "logical", - op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left: lowerer.coerceInto(expr.left, left, target), - right: lowerer.coerceInto(expr.right, right, target), - type: target, loc, - }; - } - lowerer.unsupported("SC1042", expr); - } - return { - kind: "logical", - op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", - left, right, type: left.type, loc, - }; + return lowerLogicalChain(lowerer, expr); } if (op === ts.SyntaxKind.QuestionQuestionToken) { return lowerer.lowerNullishCoalesce(expr, loc); @@ -6902,7 +6693,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // runs for EFFECT exactly as its statement lowering (JS discards its // value), the right operand is the expression's value — a seqExpr. // The validator restricts seqExpr statements to straight-line writes - // (the C emission point is mid-expression), so a left operand whose + // (the lowering point is mid-expression), so a left operand whose // statement lowering needs real control flow keeps a pointed fence // instead of tripping the validator downstream. const effect = lowerer.lowerExprStatement(expr.left); @@ -7485,20 +7276,294 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.syntaxKindName(op)}'`); } - function lowerAbsenceAwareOrChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null { - const nodes: ts.Expression[] = []; - const collect = (node: ts.Expression): void => { +function lowerLogicalChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr { + // Logical chains have a left-nested AST. Keep the native stack bounded + // while retaining each pair's checker type and lazy operand evaluation. + const parents: ts.BinaryExpression[] = []; + let current = expr; + let result: IrExpr; + while (true) { + if (current.operatorToken.kind === ts.SyntaxKind.BarBarToken) { + const fallback = lowerAbsenceAwareOrChain(lowerer, current); + if (fallback !== null) { + result = fallback; + break; + } + } + const left = current.left; + if (ts.isBinaryExpression(left) && + (left.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + left.operatorToken.kind === ts.SyntaxKind.BarBarToken) && + !lowerer.chainRecvByNode.has(left)) { + parents.push(current); + current = left; + continue; + } + result = lowerLogicalPair(lowerer, current, + lowerAbsenceProbe(lowerer, left) ?? lowerer.lowerExpr(left)); + break; + } + for (let i = parents.length - 1; i >= 0; i--) { + result = lowerLogicalPair(lowerer, parents[i]!, result); + } + return result; +} + +function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrExpr): IrExpr { + const op = expr.operatorToken.kind; + const loc = locOf(expr); + const right = lowerer.lowerExpr(expr.right); + // A LITERAL-unit left operand (a compile-time undefined/null — the + // capability-probe members: `process.features.inspector || + // !flag.startsWith('--inspect')` reads undefined in a compiled + // binary): statically falsy, so the operator folds JS-exactly — + // `unit || X` IS X, `unit && X` IS the unit (X's side effects never + // run in JS either; the lowered right is simply dropped). Literal + // units only — computed unit-typed values keep the fences below. + if (left.kind === "unitLit") { + return op === ts.SyntaxKind.BarBarToken ? right : left; + } + // JS objects and symbols are always truthy. In `ref || fallback` + // the fallback is therefore unreachable and the result is the + // already-evaluated left value — the browser-fallback idiom used by + // packages for `process.argv || []` and `process.env || {}`. The + // operand itself remains in the IR, preserving reads/calls that + // produce the reference; only the unreachable right lowering drops. + if (op === ts.SyntaxKind.BarBarToken && REF_TRUTHY_KINDS.has(left.type.kind)) { + return left; + } + if (left.type.kind === "dyn" || right.type.kind === "dyn") { + // A checked-dynamic operand (`fn.name || ''` — + // test/common's _mustCallInner): both sides live in the checked-dynamic tree and + // the deciding test is ToBoolean over the dyn kind + // (scr_dyn_truthy) — JS value semantics exactly, result dyn. The + // non-dyn side converts through the usual boundary; a value with + // no dyn representation keeps the fence. + const l = lowerer.coerceToExpected(left, DYN); + const r = lowerer.coerceToExpected(right, DYN); + if (l.type.kind !== "dyn" || r.type.kind !== "dyn") { + lowerer.unsupported("SC1100", expr, "logical operators on 'unknown' values"); + } + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: l, + right: r, + type: DYN, loc, + }; + } + if (left.type.kind === "jsval" || right.type.kind === "jsval") { + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.jsvalIn(left, expr.left), + right: lowerer.jsvalIn(right, expr.right), + type: JSVAL, loc, + }; + } + if (left.type.kind === "union" || right.type.kind === "union") { + // Test the deciding operand in its own representation. The result + // contains only the values that can survive short-circuit evaluation. + let target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + // Unchecked array reads carry undefined even when the checker sees + // a required value. The RHS can return it for either operator; the + // LHS can return it only for &&. Preserve it through later chain pairs. + if (target !== null && !isUnitType(target) && ( + (right.type.kind === "union" && lowerer.armTag(right.type.unionId, UNDEFINED_T) >= 0) || + (op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && + lowerer.armTag(left.type.unionId, UNDEFINED_T) >= 0) + )) { + target = lowerer.runtimeOptionalType(target); + } + // A broad-JSDoc npm-static body can leave the CHECKER result `any` + // even after declaration-backed specialization has recovered both + // operands (`helpOption && args.find(...)` in commander). When the + // left consists only of always-truthy references and falsy units, + // the static result is exactly the right operand plus those units. + if ( + op === ts.SyntaxKind.AmpersandAmpersandToken && + target === null && + lowerer.implicitParamTypes !== null && + npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && + (lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 && + left.type.kind === "union" + ) { + const leftArms = lowerer.unions.get(left.type.unionId)?.arms; + const rightArms = right.type.kind === "union" + ? lowerer.unions.get(right.type.unionId)?.arms + : [right.type]; + const rightIsStatic = rightArms?.every( + (arm) => arm.kind !== "dyn" && arm.kind !== "caught" && arm.kind !== "jsval" && arm.kind !== "void", + ) ?? false; if ( - ts.isBinaryExpression(node) && - node.operatorToken.kind === ts.SyntaxKind.BarBarToken + leftArms && rightArms && rightIsStatic && + leftArms.some(isUnitType) && + leftArms.every((arm) => isUnitType(arm) || REF_TRUTHY_KINDS.has(arm.kind)) ) { - collect(node.left); - collect(node.right); + const byKey = new Map(); + for (const arm of [...rightArms, ...leftArms.filter(isUnitType)]) { + byKey.set(typeKey(arm), arm); + } + const arms = [...byKey.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + if (arms.length > 1) { + const source = right.type.kind === "union" ? lowerer.unions.get(right.type.unionId) : undefined; + target = { kind: "union", unionId: source + ? lowerer.unions.transform(source, arms) + : lowerer.unions.intern(arms) }; + } + } + } + if (target?.kind === "union") { + // `&&` whose left does NOT fit the result union: the checker + // built that result by DROPPING left arms which are always + // truthy (`Option | undefined && string | undefined` answers + // `string | undefined`, not `Option | string | undefined`). + // Evaluate the left once, test it in its own union, and retag it + // only on the falsy path. Any stranded reference arms are + // unreachable there because every JS object is truthy. + if (op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && !typeEquals(left.type, target)) { + const def = lowerer.unions.get(left.type.unionId); + const trappable = new Set(); + def?.arms.forEach((arm, tag) => { + if (lowerer.armTag(target.unionId, arm) < 0 && REF_TRUTHY_KINDS.has(arm.kind)) { + trappable.add(tag); + } + }); + const retag = trappable.size === 0 + ? null + : lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc, trappable); + if (retag) { + lowerer.requireTruthyUnion(left.type.unionId, expr); + const stmts: IrStmt[] = []; + let stable: IrExpr = left; + if (!isSafeToRepeat(left)) { + const local = lowerer.declareHiddenLocal("%and", left.type); + stmts.push({ kind: "varDecl", localId: local.id, init: left, loc }); + stable = varRef(local.id, left.type, loc); + } + const result: IrExpr = { + kind: "ternary", + cond: lowerer.ensureBool(stable, expr.left), + then: lowerer.coerceInto(expr.right, right, target), + else_: { kind: "call", callee: retag, args: [stable], type: target, loc }, + type: target, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: target, loc }; + } + } + // `||` whose left does NOT fit the result union: the checker built + // that result by DROPPING the left's falsy arms (`process.env.X || + // null` is `string | null`, `|| 3000` is `string | number` — the + // `undefined` is gone from both), so coercing the left eagerly, as + // the shared shape below does, retags an arm the test is about to + // rule out and throws where Node yields the default. Single-eval + // instead: test the left in its OWN union and retag only on the + // truthy side, where the dropped arms are unreachable. + if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) { + const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc); + if (retag) { + lowerer.requireTruthyUnion(left.type.unionId, expr); + return { + kind: "orDefault", + left, + right: lowerer.coerceInto(expr.right, right, target), + retag, + type: target, + loc, + }; + } + } + lowerer.requireTruthyUnion(target.unionId, expr); + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.coerceInto(expr.left, left, target), + right: lowerer.coerceInto(expr.right, right, target), + type: target, loc, + }; + } + // `u || d` NARROWED by the default: the checker types the result + // as u's single non-unit arm (`marker() || "default"`) — evaluate + // u once, truthy extracts the arm, falsy takes d lazily (the + // orDefault node, nullish's truthiness sibling). An UNMAPPABLE + // checker result takes the same rule (`options.runner || + // defaultRunner` — tsc's union of two structurally-compatible + // function types has no representation, while the left's one + // non-unit arm is the only representable answer): the default + // must coerce into the arm or fence on its own. + if ( + op === ts.SyntaxKind.BarBarToken && + left.type.kind === "union" + ) { + const def = lowerer.unions.get(left.type.unionId); + const rest = def ? def.arms.filter((a) => !isUnitType(a)) : []; + const funcArmDefault = + rest[0]?.kind === "func" && (target === null || target.kind === "func"); + if (rest.length === 1 && ((target !== null && typeEquals(target, rest[0]!)) || funcArmDefault)) { + lowerer.requireTruthyUnion(left.type.unionId, expr); + const dflt = lowerer.lowerExprExpecting(expr.right, rest[0]!); + return { kind: "orDefault", left, right: dflt, type: rest[0]!, loc }; + } + } + // JavaScript defaults can join differently inferred object layouts. + // When both operands have native checked representations, preserve + // the deciding value and short-circuit evaluation in that domain. + if (isJsSourceFile(expr.getSourceFile()) && + lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { + return { + kind: "logical", op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.coerceInto(expr.left, left, DYN), + right: lowerer.coerceInto(expr.right, right, DYN), type: DYN, loc, + }; + } + lowerer.unsupported( + "SC1090", + expr, + `logical operators on union-typed values outside conditions (${NARROW_FIRST})`, + ); + } + const kind = left.type.kind; + if ( + kind !== right.type.kind || + (kind !== "f64" && kind !== "string" && kind !== "bool") + ) { + // Mixed PLAIN operands (`value || null`, `flag || undefined`, + // `s || 0`): JS value semantics still compile when the checker + // types the RESULT as one union both operands coerce into — the + // same lift the union-operand path above takes, arriving here + // with two plain arm values instead. + const target = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (target?.kind === "union") { + lowerer.requireTruthyUnion(target.unionId, expr); + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.coerceInto(expr.left, left, target), + right: lowerer.coerceInto(expr.right, right, target), + type: target, loc, + }; + } + lowerer.unsupported("SC1042", expr); + } + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left, right, type: left.type, loc, + }; +} + + function lowerAbsenceAwareOrChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null { + const nodes: ts.Expression[] = []; + const pending: ts.Expression[] = [expr]; + while (pending.length !== 0) { + const node = pending.pop()!; + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.BarBarToken) { + pending.push(node.right, node.left); } else { nodes.push(node); } - }; - collect(expr); + } if (nodes.length < 3) return null; const lowered = nodes.map((node) => lowerAbsenceProbe(lowerer, node) ?? lowerer.lowerExpr(node)); if (!lowered.some((value) => value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0)) { diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index bdb427eb9..6f0af989d 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -822,6 +822,13 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi if (isNativeProxyInitializer(lowerer, init)) return true; let e: ts.Expression = init; while (ts.isParenthesizedExpression(e)) e = e.expression; + // JavaScript process streams lower to native checked handles even when + // their declared method overloads prevent a structural type mapping. + // Register the shared slot before separately declared functions lower. + if (ts.isPropertyAccessExpression(e)) { + const member = lowerer.stdlibGlobalMember(e, "process"); + if (member === "stdin" || member === "stdout" || member === "stderr") return true; + } if (e.kind === ts.SyntaxKind.NullKeyword) return true; if (ts.isIdentifier(e) && e.text === "undefined") return true; if (ts.isConditionalExpression(e)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts new file mode 100644 index 000000000..c9d1331df --- /dev/null +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, JSVAL, type IrExpr, type IrStmt } from "../../ir/ir.js"; +import { stmtUsesIsland } from "./lowerer.js"; + +const loc = { file: "island.ts", start: 0, end: 1 }; +const island: IrExpr = { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc }; +const statement: IrStmt = { kind: "exprStmt", expr: island, loc }; + +test("accounts only the expressions owned by the current source statement", () => { + expect(stmtUsesIsland(statement)).toBe(true); + expect(stmtUsesIsland([statement])).toBe(true); + expect(stmtUsesIsland({ kind: "block", body: [statement], loc })).toBe(false); + expect(stmtUsesIsland({ kind: "if", cond: { kind: "boolLit", value: true, type: BOOL, loc }, then: [statement], else_: null, loc })).toBe(false); + expect(stmtUsesIsland({ kind: "if", cond: { kind: "jsExit", value: island, type: BOOL, loc }, then: [], else_: null, loc })).toBe(true); +}); + +test("inspects expression results without recounting embedded statement lists", () => { + const expr: IrExpr = { kind: "seqExpr", stmts: [statement], result: { kind: "numLit", value: 0, type: F64, loc }, type: F64, loc }; + expect(stmtUsesIsland({ kind: "exprStmt", expr, loc })).toBe(false); + expr.result = { kind: "jsExit", value: island, type: F64, loc }; + expect(stmtUsesIsland({ kind: "exprStmt", expr, loc })).toBe(true); +}); + +test("retains accounting for island library calls and generated loop conditions", () => { + const call: IrExpr = { kind: "libCall", fn: "island.eval", args: [], type: JSVAL, loc }; + const loop: IrStmt = { kind: "for", init: null, cond: { kind: "jsExit", value: call, type: BOOL, loc }, update: null, body: [], loc }; + expect(stmtUsesIsland({ kind: "block", body: [loop], loc })).toBe(true); + expect(stmtUsesIsland({ kind: "exprStmt", expr: { kind: "numLit", value: 1, type: F64, loc }, loc })).toBe(false); +}); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index cf771a048..7ce6fe781 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,5 +1,6 @@ +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { buildUnionNarrow } from "./union-narrow.js"; -import { planUnionRetag, buildUnionRetag } from "./union-retag.js"; +import { planUnionRetag, buildUnionRetag, planRecordUnionWrap, buildRecordUnionWrap } from "./union-retag.js"; import type { WidthLift } from "./width-lift.js"; export type { WidthLift } from "./width-lift.js"; import { bindingInContext, captureContextBinding, declareContextLocal, declareContextThis, type FnCtx } from "./function-context.js"; @@ -202,10 +203,6 @@ const IR_STMT_KINDS = new Set([ "continue", "block", "tryCatch", "throw", "rethrow", "runtimeFence", ]); -/** True when a lowered statement's OWN expressions contain island - * constructs — a generic JSON walk (like moduleUsesRegex): `kind` - * discriminants live only on IR objects, so user string values can never - * false-positive. Nested statements are skipped (counted separately). */ /** Every identifier a binding name binds: the identifier itself, or all * identifiers of a (possibly nested) destructuring pattern in source * order. */ @@ -221,29 +218,21 @@ export function boundIdentifiersOf(name: ts.BindingName): ts.Identifier[] { return out; } +/** Inspect executable children directly, without boxing the statement tree + * into checked-dynamic storage just to calculate coverage. */ export function stmtUsesIsland(stmts: IrStmt | IrStmt[]): boolean { - let found = false; - const visit = (v: unknown, root: boolean): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item, root); - return; - } - const kind = (v as { kind?: unknown }).kind; - if (!root && typeof kind === "string" && IR_STMT_KINDS.has(kind)) return; - const fn = (v as { fn?: unknown }).fn; - if ( - kind === "jsOp" || kind === "jsExit" || kind === "jsBridgePromise" || - fn === "island.eval" || fn === "island.import" || fn === "island.importDyn" || - fn === "island.castFail" - ) { - found = true; - return; - } - for (const value of Object.values(v)) visit(value, false); + // Keep the accounting boundary at the source statement: nested statement + // lists are counted by their own lowerStmts invocation. + const nested = (stmt: IrStmt): boolean => + IR_STMT_KINDS.has(stmt.kind) || everyStmtChild(stmt, expr, nested); + const expr = (node: IrExpr): boolean => { + if (node.kind === "jsOp" || node.kind === "jsExit" || node.kind === "jsBridgePromise" || + (node.kind === "libCall" && (node.fn === "island.eval" || node.fn === "island.import" || + node.fn === "island.importDyn" || node.fn === "island.castFail"))) return false; + return everyExprChild(node, expr, nested); }; - visit(stmts, true); - return found; + if (Array.isArray(stmts)) return !stmts.every((stmt) => everyStmtChild(stmt, expr, nested)); + return !everyStmtChild(stmts, expr, nested); } export interface LowerResult { @@ -3069,15 +3058,29 @@ export class Lowerer { ) return [0]; return null; }; - const bodyReturnsOptional = (fn: ts.FunctionLikeDeclaration): boolean => { - if (!fn.body) return false; - if (!ts.isBlock(fn.body)) return mayBeOptional(fn.body as ts.Expression); - let found = false; + // Syntax does not change during the fixed point. Reuse its return sites + // while recomputing their optionality from each pass's current bindings. + const returnSites = new Map(); + const returnsOf = (fn: ts.FunctionLikeDeclaration): ts.Expression[] => { + const cached = returnSites.get(fn); + if (cached !== undefined) return cached; + const expressions: ts.Expression[] = []; + returnSites.set(fn, expressions); + if (!fn.body) return expressions; + if (!ts.isBlock(fn.body)) { + expressions.push(fn.body as ts.Expression); + } ts.walkPreorder(fn.body, (node) => { if (node !== fn.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return "skip"; - if (ts.isReturnStatement(node) && node.expression && mayBeOptional(node.expression)) found = true; + if (ts.isReturnStatement(node) && node.expression) expressions.push(node.expression); return undefined; }); + return expressions; + }; + const bodyReturnsOptional = (fn: ts.FunctionLikeDeclaration): boolean => { + if (fn.body && !ts.isBlock(fn.body)) return mayBeOptional(fn.body as ts.Expression); + let found = false; + for (const expression of returnsOf(fn)) if (mayBeOptional(expression)) found = true; return found; }; const promoteHofCallback = (callback: ts.Expression, parameterIndices: readonly number[], seen = new Set()): boolean => { @@ -3210,9 +3213,20 @@ export class Lowerer { const decl = this.checker.valueDeclarationOf(symbol); return !!decl && ts.isVariableDeclaration(decl) && !!decl.initializer && callbackReturnsOptional(decl.initializer, seen); }; + const scanSites = new Map(); + const sitesOf = (sf: ts.SourceFile): ts.Node[] => { + const cached = scanSites.get(sf); + if (cached !== undefined) return cached; + const nodes: ts.Node[] = []; + ts.walkPreorder(sf, (node) => { + if (ts.isVariableDeclaration(node) || ts.isBinaryExpression(node) || ts.isCallExpression(node)) nodes.push(node); + }); + scanSites.set(sf, nodes); + return nodes; + }; const scanFile = (sf: ts.SourceFile): boolean => { let changed = false; - ts.walkPreorder(sf, (node) => { + const scan = (node: ts.Node): void => { if (ts.isVariableDeclaration(node)) { if (node.initializer) { if (ts.isIdentifier(node.name) && isDynamicObjectEntryRead(node.initializer)) { @@ -3411,38 +3425,23 @@ export class Lowerer { } }); } - }); + }; + for (const node of sitesOf(sf)) scan(node); return changed; }; const scanReturns = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): boolean => { - if (!decl.body) return false; let changed = false; - if (!ts.isBlock(decl.body) && mayBeOptional(decl.body as ts.Expression) && !optionalReturns.has(symbol)) { - optionalReturns.add(symbol); - changed = true; - } - if (!ts.isBlock(decl.body)) { - const arithmetic = optionalStringArithmeticType(decl.body as ts.Expression); + for (const expression of returnsOf(decl)) { + if (mayBeOptional(expression) && !optionalReturns.has(symbol)) { + optionalReturns.add(symbol); + changed = true; + } + const arithmetic = optionalStringArithmeticType(expression); if (arithmetic && !arithmeticReturns.has(symbol)) { arithmeticReturns.set(symbol, arithmetic); changed = true; } } - ts.walkPreorder(decl.body, (node) => { - if (node !== decl.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return "skip"; - if (ts.isReturnStatement(node) && node.expression && mayBeOptional(node.expression) && !optionalReturns.has(symbol)) { - optionalReturns.add(symbol); - changed = true; - } - if (ts.isReturnStatement(node) && node.expression) { - const arithmetic = optionalStringArithmeticType(node.expression); - if (arithmetic && !arithmeticReturns.has(symbol)) { - arithmeticReturns.set(symbol, arithmetic); - changed = true; - } - } - return undefined; - }); return changed; }; let changed = true; @@ -3558,7 +3557,7 @@ export class Lowerer { if (this.mixinFnShapes.get(decl)) continue; const sig = declSymbol ? this.fnSigsBySymbol.get(declSymbol) : undefined; // A body nothing reaches never lowers: its constructs can't fail - // the build and it leaves no trace in the emitted C. + // the build and it leaves no trace in the emitted LLVM. if (sig && !this.wantBody(sig.name)) continue; const fn = this.lowerFunction(decl); if (fn) functions.push(fn); @@ -5493,6 +5492,10 @@ export class Lowerer { return expr; } const tag = this.armTag(expected.unionId, expr.type); + if (tag >= 0 && expr.type.kind === "record" && expr.kind !== "recordLit") { + const helper = this.recordUnionWrapHelper(expr.type, expected.unionId, expr.loc); + if (helper) return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc }; + } if (tag < 0) { // A derived class flowing into a union with a base-class arm widens // first (nearest ancestor arm wins), then wraps like any arm value. @@ -5660,6 +5663,8 @@ export class Lowerer { * identity arms and compatible payload conversions; a shared * discriminant may select different record destinations) * wrap — a non-unit arm value into a union that contains it + * discriminantWrap — identical recursive layouts distinguished by + * their literal field value rather than their storage id * liftWrap — a record/array value into a union with NO identical arm * but exactly ONE arm it width-lifts into (the findRoute * rule applied at every level; several candidates are @@ -5692,9 +5697,13 @@ export class Lowerer { // LITERAL unit — and no lowered shape carries a bare unit field). if (isUnitType(src)) return null; const tag = this.armTag(dst.unionId, src); - if (tag >= 0) return { how: "wrap", tag }; const def = this.unions.get(dst.unionId); if (!def) return null; + if (tag >= 0) { + const shape = src.kind === "record" ? this.shapes.get(src.shapeId) : undefined; + if (shape && planRecordUnionWrap(shape, def, (id) => this.shapes.get(id))) return { how: "discriminantWrap" }; + return { how: "wrap", tag }; + } const candidates: { tag: number; arm: IrType }[] = []; def.arms.forEach((arm, i) => { if (isUnitType(arm)) return; @@ -5810,6 +5819,12 @@ export class Lowerer { if (dst.kind !== "union") throw new InternalCompilerError("lowerer bug: wrap lift against a non-union"); return { kind: "unionWrap", unionId: dst.unionId, tag: lift.tag, value, type: dst, loc }; } + case "discriminantWrap": { + if (dst.kind !== "union" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: record discriminator lift shape"); + const helper = this.recordUnionWrapHelper(value.type, dst.unionId, loc); + if (!helper) throw new InternalCompilerError("lowerer bug: planned record discriminator lift failed to intern"); + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "retag": { if (dst.kind !== "union" || value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: retag lift shape"); const retag = this.unionRetagHelper(value.type.unionId, dst.unionId, loc); @@ -7241,6 +7256,22 @@ export class Lowerer { return { kind: "call", callee: name, args: isUnitType(src) ? [] : [expr], type: expected, loc }; } + recordUnionWrapHelper(source: IrType & { kind: "record" }, toId: string, loc: SrcLoc): string | null { + const shape = this.shapes.get(source.shapeId); + const to = this.unions.get(toId); + if (!shape || !to) return null; + const plan = planRecordUnionWrap(shape, to, (id) => this.shapes.get(id)); + if (!plan) return null; + const key = `recordWrap:${source.shapeId}:${toId}`; + const existing = this.retagHelpers.get(key); + if (existing) return existing; + const name = `%record.wrap.${this.retagHelpers.size}`; + this.retagHelpers.set(key, name); + this.liftedFns.push(buildRecordUnionWrap(name, source, to, plan, loc, + (lift, value, dst) => this.applyWidthLift(lift, value, dst, loc))); + return name; + } + unionRetagHelper(fromId: string, toId: string, loc: SrcLoc, trappable?: ReadonlySet): string | null { const from = this.unions.get(fromId); const to = this.unions.get(toId); diff --git a/packages/compiler/src/frontend/lowering/union-retag.test.ts b/packages/compiler/src/frontend/lowering/union-retag.test.ts index 3bcbd7b4d..55e9ba7a2 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.test.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vitest"; -import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals } from "../../ir/ir.js"; +import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals, typeKey } from "../../ir/ir.js"; import type { IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; -import { planUnionRetag, buildUnionRetag } from "./union-retag.js"; +import { planUnionRetag, buildUnionRetag, planRecordUnionWrap } from "./union-retag.js"; import type { UnionRetagArm } from "./union-retag.js"; import type { WidthLift } from "./width-lift.js"; @@ -48,7 +48,65 @@ function split(plan: UnionRetagArm[] | null) { return first; } +describe("record conversion with shared layouts", () => { + test.each([ + [STRING, ["binary", "logical"]], [F64, [0, 1]], [BOOL, [false, true]], + ] as [IrType, (string | number | boolean)[]][])("selects %j variants by their value", (type, values) => { + const f = fixture(type, values); + f.shapes[2]!.fields = f.shapes[1]!.fields.slice(); + expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toEqual({ + field: "kind", fieldType: type, routes: [ + { tag: 0, lift: { how: "copy" }, values: [values[0]] }, + { tag: 1, lift: { how: "width" }, values: [values[1]] }, + ], + }); + // Re-read registry definitions: a later field incompatibility must + // invalidate a previously possible shared-layout conversion. + f.shapes[2]!.fields = [{ name: "kind", type }, { name: "left", type: STRING }]; + expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); + }); + + test("requires unambiguous literal ownership and matching field order", () => { + const f = fixture(); + f.shapes[2]!.fields = f.shapes[1]!.fields.slice(); + f.to.discriminant!.cases[1]!.values = ["left"]; + expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); + f.to.discriminant!.cases[1]!.values = ["right"]; + f.shapes[2]!.fields.reverse(); + expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); + }); +}); + describe("union conversion planning", () => { + test("matches large unions in source order, including duplicate destination arms", () => { + const arms: IrType[] = Array.from({ length: 48 }, (_, index) => record(`r${index}`)); + const from: IrUnionDef = { id: "large-from", arms }; + const to: IrUnionDef = { id: "large-to", arms: [record("r7"), ...arms.slice().reverse(), record("r7")] }; + const plan = planUnionRetag(from, to, () => undefined, () => null); + expect(plan).toEqual(arms.map((source) => ({ + kind: "direct", route: { tag: to.arms.findIndex((arm) => typeEquals(arm, source)), lift: { how: "copy" }, values: [] }, + }))); + to.arms = to.arms.filter((arm) => !typeEquals(arm, arms[11]!)); + expect(planUnionRetag(from, to, () => undefined, () => null)).toBeNull(); + expect(planUnionRetag(from, to, () => undefined, () => null, new Set([11]))?.[11]).toEqual({ kind: "trap" }); + }); + + test("checks exact ABI equality within type-key collisions", () => { + const fixed: IrType = { kind: "func", params: [STRING], ret: F64 }; + const withArguments: IrType = { kind: "func", params: [STRING], ret: F64, argumentsAll: true }; + expect(typeKey(fixed)).toBe(typeKey(withArguments)); + expect(typeEquals(fixed, withArguments)).toBe(false); + const padding: IrType[] = Array.from({ length: 12 }, (_, index) => record(`r${index}`)); + const from: IrUnionDef = { id: "collision-from", arms: [fixed, withArguments, ...padding] }; + const to: IrUnionDef = { id: "collision-to", arms: [withArguments, fixed, ...padding] }; + expect(planUnionRetag(from, to, () => undefined, () => null)?.slice(0, 2)).toEqual([ + { kind: "direct", route: { tag: 1, lift: { how: "copy" }, values: [] } }, + { kind: "direct", route: { tag: 0, lift: { how: "copy" }, values: [] } }, + ]); + to.arms.splice(1, 1); + expect(planUnionRetag(from, to, () => undefined, () => null)).toBeNull(); + }); + test("splits one source layout by semantic kind", () => { const f = fixture(); const arm = split(f.plan()); diff --git a/packages/compiler/src/frontend/lowering/union-retag.ts b/packages/compiler/src/frontend/lowering/union-retag.ts index eae0e86dc..19f7f7682 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.ts @@ -1,5 +1,5 @@ import { InternalCompilerError } from "../../errors.js"; -import { BOOL, F64, STRING, isUnitType, typeEquals } from "../../ir/ir.js"; +import { BOOL, F64, STRING, isUnitType, shapeHasAccessorSlots, typeEquals, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; import type { WidthLift } from "./width-lift.js"; import { discriminantField, discriminantOwners } from "../union-discriminants.js"; @@ -17,6 +17,44 @@ export type UnionRetagArm = | { kind: "direct"; route: UnionRetagRoute } | { kind: "discriminant"; field: string; fieldType: IrType; routes: UnionRetagRoute[] }; +export interface RecordUnionWrapPlan { + field: string; + fieldType: IrType; + routes: UnionRetagRoute[]; +} + +/** Recursive variants can retain distinct ids for the same field layout. + * An independently inferred record may intern to either id, so that id + * alone cannot select its semantic destination when wrapping into a union. */ +export function planRecordUnionWrap( + source: IrRecordShape, + to: IrUnionDef, + shapeOf: (id: string) => IrRecordShape | undefined, +): RecordUnionWrapPlan | null { + const discriminant = to.discriminant; + if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source)) return null; + const fieldType = discriminantField(source, discriminant.field); + if (!fieldType) return null; + const routes: UnionRetagRoute[] = []; + for (let tag = 0; tag < to.arms.length; tag++) { + const arm = to.arms[tag]!; + if (arm.kind !== "record") continue; + const target = shapeOf(arm.shapeId); + if (!target || target.tuple || target.indexValue || target.fields.length !== source.fields.length) continue; + let same = true; + for (let i = 0; i < source.fields.length; i++) { + const a = source.fields[i]!, b = target.fields[i]!; + if (a.name !== b.name || !typeEquals(a.type, b.type)) { same = false; break; } + } + if (!same) continue; + const entry = discriminant.cases.find((candidate) => candidate.tag === tag); + if (!entry) return null; + routes.push({ tag, lift: { how: arm.shapeId === source.id ? "copy" : "width" }, values: entry.values }); + } + if (routes.length < 2 || discriminantOwners(to, shapeOf) === null) return null; + return { field: discriminant.field, fieldType, routes }; +} + /** Pure conversion planning. A source storage tag need not denote just * one semantic variant: refinements can coalesce recursive record layouts. * When both unions name the same discriminant, its values select target @@ -35,9 +73,22 @@ export function planUnionRetag( const useDiscriminant = discriminant !== undefined && discriminant.field === to.discriminant?.field; const owners = useDiscriminant ? discriminantOwners(to, shapeOf) : null; if (useDiscriminant && (owners === null || discriminantOwners(from, shapeOf) === null)) return null; + // Large unions otherwise compare every source arm with every destination. + // Keys narrow the search; exact ABI equality still resolves collisions. + // Keep small conversions allocation-light and rebuild for each registry view. + const indexed = from.arms.length >= 4 && to.arms.length >= 8; + const identities = new Map(); + if (indexed) to.arms.forEach((arm, tag) => { + const key = typeKey(arm); + const tags = identities.get(key); + if (tags) tags.push(tag); + else identities.set(key, [tag]); + }); for (let tag = 0; tag < from.arms.length; tag++) { const source = from.arms[tag]!; - const identity = to.arms.findIndex((arm) => typeEquals(arm, source)); + const identity = indexed + ? identities.get(typeKey(source))?.find((candidate) => typeEquals(to.arms[candidate]!, source)) ?? -1 + : to.arms.findIndex((arm) => typeEquals(arm, source)); if (isUnitType(source)) { result.push(identity < 0 ? { kind: "trap" } : { kind: "direct", route: { tag: identity, lift: { how: "copy" }, values: [] }, @@ -114,6 +165,42 @@ function literalTest(field: IrExpr, literal: Literal, loc: SrcLoc): IrExpr { return { kind: "bin", op: "===", left: field, right, type: BOOL, loc }; } +export function buildRecordUnionWrap( + name: string, + source: IrType & { kind: "record" }, + to: IrUnionDef, + plan: RecordUnionWrapPlan, + loc: SrcLoc, + applyLift: (lift: WidthLift, value: IrExpr, dst: IrType) => IrExpr, +): IrFunction { + const input: IrExpr = { kind: "varRef", localId: "v.0", type: source, loc }; + const field: IrExpr = { kind: "varRef", localId: "kind.0", type: plan.fieldType, loc }; + const result: IrType = { kind: "union", unionId: to.id }; + const body: IrStmt[] = [{ kind: "varDecl", localId: "kind.0", init: { + kind: "recordGet", obj: input, shapeId: source.shapeId, field: plan.field, type: plan.fieldType, loc, + }, loc }]; + for (const route of plan.routes) { + let cond: IrExpr | null = null; + for (const literal of route.values) { + const test = literalTest(field, literal, loc); + cond = cond === null ? test : { kind: "logical", op: "||", left: cond, right: test, type: BOOL, loc }; + } + if (cond === null) throw new InternalCompilerError("lowerer bug: empty record discriminator route"); + body.push({ kind: "if", cond, then: [{ kind: "return", value: { + kind: "unionWrap", unionId: to.id, tag: route.tag, + value: applyLift(route.lift, input, to.arms[route.tag]!), type: result, loc, + }, loc }], else_: null, loc }); + } + body.push(typeError(`invalid '${plan.field}' discriminant in record conversion`, loc)); + return { + name, params: [{ localId: "v.0", name: "value", type: source }], returnType: result, + locals: [ + { id: "v.0", name: "value", type: source, mutable: false }, + { id: "kind.0", name: "kind", type: plan.fieldType, mutable: false }, + ], body, loc, + }; +} + /** Emit the already validated routes using ordinary typed IR. Each field * read uses the source shape; each result uses its own destination shape. * Identity routes retain the original payload and width routes copy it. diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index 3e63d5e90..40c533d72 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -6,6 +6,7 @@ import type { IrType } from "../../ir/ir.js"; export type WidthLift = | { how: "copy" } | { how: "wrap"; tag: number } + | { how: "discriminantWrap" } | { how: "retag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } diff --git a/packages/compiler/src/frontend/npm.ts b/packages/compiler/src/frontend/npm.ts index b9dde60ff..960165688 100644 --- a/packages/compiler/src/frontend/npm.ts +++ b/packages/compiler/src/frontend/npm.ts @@ -16,7 +16,7 @@ * ts.preProcessFile silently drops), dynamic import() of string literals, * and CommonJS require() calls — never a regex. * - * Every reached module's SOURCE is embedded into the emitted C as a static + * Every reached module's SOURCE is embedded into the emitted LLVM as a static * string keyed by its REALPATH — one physical module embeds once, however * many symlink aliases reach it; scr_island.c serves the engine's module * loader (and a CommonJS require shim) from that map, so binaries never diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 174b39ff2..5dc576967 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -36,7 +36,7 @@ export type ProgramLoader = (entryPath: string, options: ProgramLoadOptions) => * Frontend shape. */ export interface Frontend { preflight: ScrDiagnostic[]; - /** The entry source file's text (emitCModule's header comment input). */ + /** The entry source file's text for diagnostics and source annotations. */ entryText: () => string; /** Library mode's resolution input: the entry file's exported function * declarations (call before dispose — it reads the ts7 AST). */ diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 9aaf16da9..15562b75e 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -1,8 +1,9 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { expect, test } from "vitest"; -import { checkPreflight, loadProgram } from "./program-node.js"; +import { expect, test, vi } from "vitest"; +import { checkPreflight, loadProgram, makeCycleAdmission, type CycleEdge } from "./program-node.js"; +import * as ts from "./ts7/ast.js"; function requireOrderDiagnostics(source: string, dependency = "exports.value = 'ready';\n") { const dir = mkdtempSync(join(tmpdir(), "scriptc-require-order-")); @@ -122,3 +123,60 @@ function read() { return dep.value; } `, "const main = require('./main.cjs'); exports.value = main.read();\n"); expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true); }); + +test.for([ + ['export function read() { return first + alias; }', null], + ['export { first, alias }; export function read() { return 1; }', null], + ['type Value = typeof alias; export function read(first: number = 1) { return first; }', null], + ['class Box { value = alias; } export function read(value = first) { return value; }', null], + ['const value = alias; export function read() { return value; }', "alias"], + ['class Box { static value = first; } export function read() { return 1; }', "first"], + ['class Box { [alias] = 1; } export function read() { return 1; }', "alias"], +])("cycle binding indexes preserve initialization safety: %s", ([body, refused]) => { + const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-bindings-")); + const entry = join(directory, "main.ts"); + writeFileSync(entry, 'import { read } from "./peer.ts"; export const first = 1; export const second = 2; export function run() { return read(); }'); + writeFileSync(join(directory, "peer.ts"), 'import { first, second as alias } from "./main.ts";\n' + body); + const load = loadProgram(entry); + try { + const cycles = checkPreflight(load).filter((diagnostic) => diagnostic.code === "SC1016"); + if (refused === null) expect(cycles).toEqual([]); + else { + expect(cycles).toHaveLength(1); + expect(cycles[0]!.message).toContain(`binding '${refused}' is read`); + } + } finally { + load.dispose(); + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("repeated cycle edges reuse a file's binding index", () => { + const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-index-")); + const entry = join(directory, "main.ts"); + writeFileSync(entry, 'import { read } from "./peer.ts"; export const first = 1; export const second = 2; export function run() { return read(); }'); + writeFileSync(join(directory, "peer.ts"), 'import { first } from "./main.ts"; import { second } from "./main.ts"; export function read() { return first + second; }'); + const load = loadProgram(entry); + try { + const main = load.entry; + const peer = load.program.getSourceFile(join(directory, "peer.ts"))!; + const mainImports = main.statements.filter(ts.isImportDeclaration); + const peerImports = peer.statements.filter(ts.isImportDeclaration); + const edges = new Map([ + [main, mainImports.map((stmt) => ({ dep: peer, stmt }))], + [peer, peerImports.map((stmt) => ({ dep: main, stmt }))], + ]); + const admit = makeCycleAdmission(load.program, (file) => edges.get(file) ?? []); + expect(admit(peer, edges.get(peer)![0]!)).toBeNull(); + // The first edge may materialize/check the file. A later binding must + // inspect its indexed references without another full-file walk. + const walk = vi.spyOn(peer, "forEachChild"); + try { + expect(admit(peer, edges.get(peer)![1]!)).toBeNull(); + expect(walk).not.toHaveBeenCalled(); + } finally { walk.mockRestore(); } + } finally { + load.dispose(); + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index e03087ecb..cd22eb101 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1884,8 +1884,8 @@ function identifierOccurrences7(root: ts.Node, text: string): ts.Identifier[] { * of an imported binding are the same alias story, not reads. */ function backEdgeUseOffence7( program: ts.Program, - sf: ts.SourceFile, stmt: ts.ImportDeclaration | ts.ExportDeclaration, + occurrences: ReadonlyMap, ): { name: string; node: ts.Node } | null { if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) return null; const checker = program.getTypeChecker(); @@ -1899,25 +1899,18 @@ function backEdgeUseOffence7( for (const bindingName of bindingNames) { const sym = checker.getSymbolAtLocation(bindingName); if (sym === undefined) continue; - checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, bindingName.text)); - let offence: { name: string; node: ts.Node } | null = null; - const visit = (node: ts.Node): void => { - if (offence !== null || ts.isImportDeclaration(node)) return; + const references = occurrences.get(bindingName.text) ?? []; + checker.prefetchSymbolNodesExact(references); + for (const node of references) { if ( - ts.isIdentifier(node) && - node.text === bindingName.text && !(node.parent !== undefined && ts.isExportSpecifier(node.parent)) && checker.getSymbolAtLocation(node) === sym && !inTypePosition7(node) && !inDeferredPosition7(node) ) { - offence = { name: bindingName.text, node }; - return; + return { name: bindingName.text, node }; } - ts.forEachChild(node, visit); - }; - visit(sf); - if (offence !== null) return offence; + } } return null; } @@ -1988,6 +1981,26 @@ export function makeCycleAdmission( // reason the cluster's cycles stay fenced, or null when its every // member passes the inert-top-level bar. const sccVerdict = new Map(); + // A module can close many cycle edges, each importing many bindings. + // Index its identifiers once for this admission pass. ASTs are immutable; + // the pass owns the index so it cannot retain a disposed program. + const bindingUses = new Map>(); + const usesOf = (sf: ts.SourceFile): Map => { + const cached = bindingUses.get(sf); + if (cached !== undefined) return cached; + const uses = new Map(); + ts.walkPreorder(sf, (node) => { + if (ts.isImportDeclaration(node)) return "skip"; + if (ts.isIdentifier(node)) { + const references = uses.get(node.text); + if (references !== undefined) references.push(node); + else uses.set(node.text, [node]); + } + return undefined; + }); + bindingUses.set(sf, uses); + return uses; + }; return (importer: ts.SourceFile, e: CycleEdge): string | null => { if (e.stmt === undefined) return "the cycle closes through a require() edge"; // Cheap per-edge admission: nothing readable binds through the edge. @@ -2026,7 +2039,7 @@ export function makeCycleAdmission( const clusterReason = sccVerdict.get(comp); if (clusterReason === undefined) throw new Error("missing module-cycle verdict"); if (clusterReason !== null) return clusterReason; - const use = backEdgeUseOffence7(program, importer, e.stmt); + const use = backEdgeUseOffence7(program, e.stmt, usesOf(importer)); if (use !== null) { return `the cycle-crossing binding '${use.name}' is read at ${lineOf(use.node)}, outside any function body — a read during the init window observes the partially-initialized module (Node's TDZ ReferenceError / stale var), which is not modeled`; } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 1e5a61b28..1755f2192 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -5,7 +5,8 @@ import { dirname, join } from "node:path"; import { afterAll, beforeAll, expect, test } from "vitest"; import type { SourceFile, Node } from "typescript/unstable/ast"; import { AstFile, AstNode } from "./ast-node.js"; -import { AstKind, KIND_NODE_LIST, astChildNames, HEADER_OFFSET_NODES, NODE_LEN } from "./ast-schema.generated.js"; +import { AstKind, KIND_NODE_LIST, astChildNames, HEADER_OFFSET_NODES, NODE_LEN, NODE_OFFSET_PARENT } from "./ast-schema.generated.js"; +import { walkPreorder } from "./ast.js"; import { ts7Executable } from "./rpc-api.js"; import { Ts7RpcClient } from "./rpc-client.js"; import { spawnTs7Wire } from "./rpc-process.js"; @@ -178,3 +179,44 @@ test("a source view requires a source-file root", () => { expect(() => new AstFile(bytes).sourceFile).toThrow("expected a source file root"); expect(decoded.get("empty.ts")!.file.root.statements).toEqual([]); }); + +test("preorder traversal preserves depth, skipped subtrees and early termination", () => { + const { file, oracle } = decoded.get("main.ts")!; + const expected: [number, number][] = []; + const visit = (node: OracleNode, depth: number): void => { + expected.push([node.index, depth]); + if (node.kind === AstKind.ClassDeclaration) return; + node.forEachChild((child) => { visit(child as OracleNode, depth + 1); }); + }; + visit(oracle.getOrCreateNodeAtIndex(1), 0); + const actual: [number, number][] = []; + walkPreorder(file.root, (node, depth) => { + actual.push([node.index, depth]); + return node.kind === AstKind.ClassDeclaration ? "skip" : undefined; + }); + expect(actual).toEqual(expected); + const prefix: [number, number][] = []; + walkPreorder(file.root, (node, depth) => { + prefix.push([node.index, depth]); + if (prefix.length === 7) return "stop"; + return node.kind === AstKind.ClassDeclaration ? "skip" : undefined; + }); + expect(prefix).toEqual(expected.slice(0, 7)); +}); + +test("child traversal still rejects a sibling belonging to another parent", () => { + const { file, bytes } = decoded.get("main.ts")!; + let parent = 0; + let sibling = 0; + for (let index = 1; index < file.wire.nodeCount; index++) { + if (file.wire.kind(index) === KIND_NODE_LIST) continue; + const children = file.wire.children(index); + if (children.length > 1) { parent = index; sibling = children[1]!; break; } + } + expect(sibling).toBeGreaterThan(0); + const broken = bytes.slice(); + const words = new DataView(broken.buffer, broken.byteOffset, broken.byteLength); + const nodes = words.getUint32(HEADER_OFFSET_NODES, true); + words.setUint32(nodes + sibling * NODE_LEN + NODE_OFFSET_PARENT, parent === 1 ? 0 : 1, true); + expect(() => new AstFile(broken).node(parent).forEachChild(() => undefined)).toThrow("sibling belongs to another parent"); +}); diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 8c44319d9..7f2065537 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -133,8 +133,12 @@ export class AstNode { } forEachChild(visitNode: (node: AstNode) => T, visitList?: (nodes: readonly AstNode[]) => T): T | undefined { - for (const index of this.file.wire.children(this.index)) { - const kind = this.file.wire.kind(index); + const wire = this.file.wire; + // Walk the immutable sibling links directly. Repeated semantic scans + // should not allocate and free a temporary child-index array per node. + for (let index = wire.firstChild(this.index); index !== 0; index = wire.next(index)) { + if (wire.parent(index) !== this.index) throw new AstDecodeError("sibling belongs to another parent"); + const kind = wire.kind(index); if (kind === KIND_NODE_LIST) { const list = this.file.list(index); if (visitList !== undefined) { diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 9bc402b1f..8ad9957b3 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -109,10 +109,12 @@ export function walkPreorder( root: Node, cb: (node: Node, depth: number) => void | "skip" | "stop", ): void { - const stack: [Node, number][] = [[root, 0]]; + const stack: Node[] = [root]; + const depths: number[] = [0]; const children: Node[] = []; while (stack.length > 0) { - const [n, depth] = stack.pop()!; + const n = stack.pop()!; + const depth = depths.pop()!; const verdict = cb(n, depth); if (verdict === "stop") return; if (verdict === "skip") continue; @@ -120,7 +122,10 @@ export function walkPreorder( n.forEachChild((c) => { children.push(c); }); - for (let i = children.length - 1; i >= 0; i--) stack.push([children[i]!, depth + 1]); + for (let i = children.length - 1; i >= 0; i--) { + stack.push(children[i]!); + depths.push(depth + 1); + } } } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index dfa3330ea..d09e2a735 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,22 +1,18 @@ +import { compilePackedLibrary } from "./backend/library-pack.js"; import { InternalCompilerError } from "./errors.js"; -import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; import { clearCcCaches, configuredTargetPlatform, type NativeArtifactDependency } from "./backend/native-toolchain.js"; import { buildCacheRoot, prepareBuildCacheRoot, pruneBuildCache } from "./backend/build-cache.js"; import { - assertLegacyCExecutablePipelineEnabled, CcCompileError, compileExternalC, compileExternalCLibrary, executableNativeEnvironmentFingerprint, - legacyCExecutablePathRequested, mobileLibraryTarget, mobileTargetRefusal, resolveCc, - targetPlatform, } from "./backend/external-c.js"; -import { emitCModule, emitCModuleChunks } from "./backend/c/c-emitter.js"; import { emitLlvmModule, LlvmUnsupportedError } from "./backend/llvm/emitter.js"; import { emitNativeArtifact, NativeCodegenError } from "./backend/native-codegen.js"; import { privateSiblingPath } from "./backend/build-cache.js"; @@ -32,7 +28,7 @@ import { resolvePlatformLinker, } from "./backend/linker.js"; import { splitLlvmLibraryProgram, splitLlvmProgram } from "./backend/llvm/split.js"; -import { rebaseLibrarySourceComments, replaceLibraryIdentity, stripLibraryIdentity, stripLibrarySourceComments } from "./backend/library-identity-markers.js"; +import { emitLibraryIdentityLines, replaceLibraryIdentity, stripLibraryIdentity } from "./backend/library-identity-markers.js"; import { checkerPanicDiag, ffiNativeBuildDiag, libAsyncExportDiag, libAsyncSurfaceDiag, libExportUnresolvedDiag, libGenericExportDiag, libIntBoundaryDiag, libNpmIneligibleDiag, libSidecarDiag, libUnmappableSignatureDiag, iceDiag, isCheckerPanic, LIB_INBOUND_BYTES_TRAP_CODE, LIB_RUNTIME_TRAP_CODES, nativeCodegenDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js"; import { checkLibraryIntegerSlots, classSeed, hasIntSlots, numberCarrierKind, type FnIntSlots, type IntSlotConfig } from "./library/int-infer.js"; import { loadLibraryProfile, profileRemediation, profileTeaching, type LibraryProfile } from "./library/library-profile.js"; @@ -53,8 +49,7 @@ import { import { validateSidecar } from "./library/sidecar-validate.js"; import type { EntryExportInfo } from "./frontend/lib-exports.js"; import type { ContractFacts } from "./frontend/lib-contract.js"; -import { moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleUsesAssert, moduleUsesCopying, moduleUsesEmitter, moduleUsesFetch, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSearchParams, moduleUsesSymbol, moduleUsesZlib, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js"; -import { moduleUsesBigInt } from "./ir/ir.js"; +import { moduleRuntimeFeatures, moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleUsesFetch, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js"; import { serializeModule } from "./ir/serialize.js"; import { validateModule } from "./ir/validate.js"; import { loadProgram } from "./frontend/program-node.js"; @@ -67,7 +62,6 @@ import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js"; import { executableLinkFeatures } from "./backend/executable-features.js"; import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js"; import { libraryFrontendImplementationFingerprint, publishEarlyLibraryCache, readEarlyLibraryCache, readSemanticLibraryCache, type EarlyLibraryCacheOptions, type EarlyLibraryCachePublish, type EarlyLibraryNativeFeatures, type SemanticLibraryCacheHit } from "./library/library-cache.js"; -import { createSourceLineRebaser } from "./library/semantic-source.js"; import { publishEarlyExecutableCache, publishEarlyExecutableRoute, readEarlyExecutableCache, type EarlyExecutableCacheOptions, type EarlyExecutableNativeFeatures } from "./executable/executable-cache.js"; import { compilerImplementationIdentity } from "./library/compiler-self-identity.js"; @@ -86,19 +80,14 @@ export { compileExternalC as compileC, compileExternalC, runtimeSrcDir, - warmNativeCaches, type CcOptions, type NativeCacheWarmProfile, type WarmNativeCachesOptions, type WarmNativeCachesResult, } from "./backend/external-c.js"; +export { warmNativeCaches } from "./backend/warm-cache.js"; export { ANDROID_MIN_API, IPHONEOS_MIN_VERSION, isAndroidTarget, isIosTarget, isMobileTarget, mobileLibraryTarget, mobileTargetRefusal } from "./backend/external-c.js"; -export { - emitCModule, - emitCModuleChunks, - emitCModule as emitModule, - type CEmitOptions, -} from "./backend/c/c-emitter.js"; +export { emitLlvmModule, type LlvmTargetOptions } from "./backend/llvm/emitter.js"; export type { ScrDiagnostic } from "./diagnostics/diagnostic.js"; export { renderDiagnostics, @@ -187,7 +176,7 @@ export { } from "./frontend/provenance-registry.js"; export * as ir from "./ir/ir.js"; -export type CompileOutputKind = "ir" | "c" | "llvm" | "asm" | "obj" | "exe"; +export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe"; export interface CompileBaseOptions { /** Primary artifact path. The CLI supplies the output-kind default. */ @@ -204,21 +193,8 @@ export interface CompileBaseOptions { * island constructs are diagnostics and nothing about codegen or linking * changes. */ dynamic?: boolean; - /** Code generator for the program TU. Unset (the release default): the - * LLVM backend emits LLVM IR text (.ll). Supported macOS arm64 builds send - * it through the bundled helper and link a precompiled runtime pack; other - * targets retain their established compiler-driver path. A program outside - * the LLVM tier falls back to the debugging C backend transparently — the IR is - * backend-agnostic, so only the emit retries; CompileResult records the - * lane (`backend`, plus `llvmRefusal` when the fallback engaged). ONLY a - * tier refusal (LlvmUnsupportedError) falls back — every real diagnostic - * and every ICE fails the build on either lane. Explicit `llvm` is the - * debugging/CI pin and keeps the fail-loudly contract: an out-of-tier - * program is diagnostic SC3001 naming the first unsupported construct, - * never a silent lane change. Explicit `c` pins the debugging C backend. - * wasm32-wasi is a production LLVM target and never takes the automatic - * C fallback; a missing LLVM lowering there is SC3001. */ - backend?: "c" | "llvm"; + /** LLVM is the production code generator. */ + backend?: "llvm"; /** Native optimization posture. Release is the shipped -O2 default; dev * uses -O0, source line tables, and stable multi-TU object caching for * large LLVM programs. Darwin executables include an adjacent .dSYM. */ @@ -271,7 +247,6 @@ export interface CompileRequestOptions extends CompileBaseOptions { export type CompileArtifact = | { kind: "ir"; path: string } - | { kind: "c"; path: string } | { kind: "llvm"; path: string } | { kind: "asm"; path: string } | { kind: "obj"; path: string; nativeLinkInfo?: NativeLinkInfo } @@ -279,8 +254,8 @@ export type CompileArtifact = kind: "exe"; path: string; translationUnitPath: string; - backend: "c" | "llvm"; - llvmRefusal?: string; + backend: "llvm"; + }; export type CompileFailure = { @@ -290,7 +265,7 @@ export type CompileFailure = { }; export type CompileSourceResult = - | { ok: true; artifact: Extract } + | { ok: true; artifact: Extract } | CompileFailure; /** Historical executable result shape retained for source compatibility. */ @@ -298,22 +273,15 @@ export type CompileResult = | { ok: true; binaryPath: string; - cPath: string; + llvmPath: string; irPath?: string; - backend: "c" | "llvm"; - llvmRefusal?: string; + backend: "llvm"; + } | CompileFailure; export type CompileExecutableResult = - /** `cPath` is the generated program TU next to the binary: the .ll under - * the LLVM backend (the default lane), the .c under the C backend (same - * seat, same lifecycle — --keep-c in the CLI governs both). `backend` is - * the code generator that ACTUALLY emitted the TU; `llvmRefusal` is - * present iff the default lane fell back to C, carrying the tier - * refusal's machine-readable kind tag ("stmt:...", "libCall:...", ...). - * These top-level fields remain as compatibility aliases for callers - * written before the discriminated `artifact` result was introduced. */ + /** The generated LLVM source is retained beside the executable. */ | (Extract & { artifact: Extract; }) @@ -334,22 +302,6 @@ function llvmRefusalDiag(err: LlvmUnsupportedError, entryPath: string): ScrDiagn }; } -/** A valid IR surface the explicitly-selected code generator cannot host. - * SC3001 is backend coverage (as with an LLVM tier refusal), not a target - * capability gap: wasm32-wasi's production LLVM lane still accepts it. */ -function backendRefusalDiag( - backend: "c" | "llvm", - target: string, - surface: string, - loc: SrcLoc, -): ScrDiagnostic { - return { - code: "SC3001", - message: `${backend} backend does not support ${surface} for ${target}; use --backend llvm`, - loc, - }; -} - /** A valid program surface that the selected execution target cannot host. * SC3xxx stays the backend/target-coverage family: source semantics are * valid, but this target deliberately refuses them instead of emitting a @@ -474,18 +426,9 @@ export interface AnalyzeResult { sourceTexts: Map; } -/** The platform the BUILD is for — the SCRIPTC_TARGET triple's OS under a - * cross compile, the host's otherwise. The frontend needs it too (the - * whole program compiles for ONE platform, so path.sep / os.EOL literals - * and the path-module binding are compile-time constants); a malformed - * SCRIPTC_CC/SCRIPTC_TARGET combination reports at compileC exactly as - * before, so analysis falls back to the host here rather than throwing. */ +/** Platform semantics depend on the output target, without compiler discovery. */ export function buildTargetPlatform(env: NodeJS.ProcessEnv = process.env): string { - try { - return targetPlatform(resolveCc(env)); - } catch { - return process.platform; - } + return configuredTargetPlatform(env); } /** Target-platform classification without compiler or SDK discovery. Source @@ -598,7 +541,7 @@ export function analyze(entryPath: string, opts: AnalyzeOptions = {}): AnalyzeRe } } -/** The whole pipeline: load → preflight → lower → validate → emit C → clang. */ +/** The whole pipeline: load → preflight → lower → validate → emit LLVM → link. */ function clearCompileSessionCaches(): void { clearResolveCaches(); clearCcCaches(); @@ -646,22 +589,20 @@ void assertCompileResultCompatibility; function executableNativeFeatures( mod: IrModule, - backend: "c" | "llvm", + backend: "llvm", dynamic: boolean, optimization: "release" | "dev", - llvmRefusal?: string, ): EarlyExecutableNativeFeatures { return { backend, ...(optimization === "dev" ? { optimization: "dev" as const } : {}), - ...(llvmRefusal === undefined ? {} : { llvmRefusal }), ...executableLinkFeatures(mod, dynamic), }; } async function compileExecutableNative( features: EarlyExecutableNativeFeatures, - cPath: string, + llvmPath: string, outPath: string, sanitize: boolean, ffi: FfiProfile | null, @@ -671,14 +612,14 @@ async function compileExecutableNative( programObjectDependencies: readonly NativeArtifactDependency[] = [], onArtifactReady?: NonNullable[0]["onArtifactReady"]>, ): Promise { - const programIsObject = /\.(?:o|obj)$/.test(cPath); - const runtimePackTarget = programIsObject && !sanitize && process.env["SCRIPTC_RUNTIME_PACK"] !== "0" + const programIsObject = /\.(?:o|obj)$/.test(llvmPath); + const runtimePackTarget = programIsObject && !sanitize ? nativeCodegenTarget() : null; if (runtimePackTarget !== null) { const plan = await createNativeLinkPlan({ target: runtimePackTarget, - programObject: cPath, + programObject: llvmPath, outPath, features, ffi, @@ -705,19 +646,11 @@ async function compileExecutableNative( const effectiveProgramSplit = programSplit ?? (!programIsObject && features.optimization === "dev" && features.backend === "llvm" && !sanitize - ? splitLlvmProgram(await readFile(cPath, "utf8")) + ? splitLlvmProgram(await readFile(llvmPath, "utf8")) : null); - const objectLinkDir = programIsObject - ? await mkdtemp(join(tmpdir(), "scriptc-object-link-")) - : null; - const linkDriverSource = objectLinkDir === null - ? cPath - : join(objectLinkDir, "driver.c"); - if (objectLinkDir !== null) await writeFile(linkDriverSource, "/* scriptc object link driver */\n"); - try { - assertLegacyCExecutablePipelineEnabled(); - await compileExternalC({ - cPath: linkDriverSource, + if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); + await compileExternalC({ + cPath: llvmPath, outPath, cacheIdentity: "scriptc-generated-v1", ...(features.optimization === "dev" ? { optimization: "dev" as const } : {}), @@ -761,21 +694,8 @@ async function compileExecutableNative( tls: features.tls, tlsCa: features.tlsCa, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), - ...(ffi === null && !programIsObject - ? {} - : { - linkInputs: [ - ...(programIsObject ? [cPath] : []), - ...(ffi?.libraries ?? []), - ], - ...(ffi === null ? {} : { systemLibraries: ffi.systemLibraries, frameworks: ffi.frameworks }), - }), + ...(ffi === null ? {} : { linkInputs: ffi.libraries, systemLibraries: ffi.systemLibraries, frameworks: ffi.frameworks }), }); - } finally { - if (objectLinkDir !== null) { - await rm(objectLinkDir, { recursive: true, force: true }).catch(() => undefined); - } - } } async function emitNativeProgramObject( @@ -807,12 +727,11 @@ async function emitNativeProgramObject( function usesPrecompiledRuntimePack( opts: CompileRequestOptions, - backend: "c" | "llvm", + backend: "llvm", ): boolean { if ( backend !== "llvm" || opts.sanitize === true || - process.env["SCRIPTC_RUNTIME_PACK"] === "0" || - process.env["SCRIPTC_FETCH_CURL"] === "1" || legacyCExecutablePathRequested() + process.env["SCRIPTC_FETCH_CURL"] === "1" ) return false; return nativeCodegenTarget() !== null; } @@ -821,6 +740,190 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD return nativeCodegenDiag(error.code === "unsupported" ? "SC3002" : "SC3003", error.message, entryPath); } +interface PreparedExecutable { + llvmSource: string; + llvmPath: string; + irPath: string | undefined; + nativeFeatures: EarlyExecutableNativeFeatures; + programSplit: ReturnType; + sourceTexts: Map; +} + +/** Finish frontend work in a separate scope so the AST, checker caches and + * typed IR can be reclaimed before the native optimizer needs its heap. */ +async function prepareExecutableInput( + entryPath: string, opts: CompileRequestOptions, ffi: FfiProfile | null, buildPlatform: string, +): Promise { + const outputKind = opts.outputKind ?? "exe"; + const fe = runFrontend(entryPath, loadProgram, opts.npmStatic); + let lowered: LowerResult; + let sourceTexts: Map; + // The frontend (and its tsgo server) is released as soon as lowering + // ends — clang and the link never hold it open. + try { + const fail = (diagnostics: ScrDiagnostic[]): CompileFailure => ({ + ok: false, + diagnostics, + sourceTexts: fe.sourceTexts(), + }); + + if (fe.preflight.length > 0) return fail(fe.preflight); + + try { + lowered = fe.lower({ + dynamic: opts.dynamic ?? false, + targetPlatform: buildPlatform, + ...(ffi !== null ? { ffiImports: ffi.functions } : {}), + }); + } catch (e) { + // The last-resort panic fence: an upstream tsgo panic that crossed a + // checker call no statement/collection fence wrapped still becomes a + // clean failed compile (anchored at the entry), never a crashed CLI. + if (!isCheckerPanic(e)) throw e; + return fail([ + checkerPanicDiag(e.message.split("\n", 1)[0]!, { file: entryPath, start: 0, end: 0 }), + ]); + } + if (lowered.module === null) return fail(lowered.diagnostics); + + const validation = validateModule(lowered.module); + if (validation.length > 0) { + return fail(validation.map((v) => iceDiag(v.message, v.loc))); + } + if (buildPlatform === "wasi") { + const entryLoc: SrcLoc = { file: entryPath, start: 0, end: 0 }; + if (opts.sanitize) { + return fail([targetRefusalDiag("wasm32-wasi", "--sanitize", entryLoc)]); + } + if (ffi !== null) { + return fail([targetRefusalDiag("wasm32-wasi", "native FFI manifests", entryLoc)]); + } + const unavailable = moduleWasiUnavailableSurface(lowered.module); + if (unavailable !== null) { + return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]); + } + } + sourceTexts = fe.sourceTexts(); + } finally { + fe.dispose(); + } + + const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); + const defaultSourcePaths = { + ir: join(opts.outDir, `${stem}.ir.json`), + llvm: join(opts.outDir, `${stem}.ll`), + } as const; + const debugOptions = opts.optimization === "dev" && !opts.strip + ? { debugSources: sourceTexts } + : {}; + + if (outputKind === "ir") { + await mkdir(dirname(opts.outPath), { recursive: true }); + await writeFile(opts.outPath, serializeModule(lowered.module)); + return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; + } + + if (outputKind === "llvm" || outputKind === "asm" || outputKind === "obj") { + let llvm: string; + try { + llvm = emitLlvmModule(lowered.module, { + targetTriple: process.env["SCRIPTC_TARGET"] ?? "", + ...debugOptions, + pointerBits: buildPlatform === "wasi" ? 32 : 64, + wasi: buildPlatform === "wasi", + runtimeAbiMarker: outputKind === "obj", + }); + } catch (err) { + if (!(err instanceof LlvmUnsupportedError)) throw err; + return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; + } + if (outputKind === "llvm") { + await mkdir(dirname(opts.outPath), { recursive: true }); + await writeFile(opts.outPath, llvm); + } else { + try { + await emitNativeArtifact({ + outputPath: opts.outPath, + llvm, + outputKind, + sourcePath: entryPath, + optimization: opts.optimization === "dev" ? "0" : "2", + ...(opts.sanitize === undefined ? {} : { sanitize: opts.sanitize }), + }); + } catch (err) { + if (!(err instanceof NativeCodegenError)) throw err; + return { + ok: false, + diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], + sourceTexts, + }; + } + } + if (outputKind === "obj" && opts.nativeLinkInfo === true) { + const target = nativeCodegenTarget(); + if (target === null) { + throw new InternalCompilerError("native object emitted without a native target"); + } + return { + ok: true, + artifact: { + kind: "obj", + path: opts.outPath, + nativeLinkInfo: await createNativeLinkInfo({ + programObject: opts.outPath, + target, + features: executableNativeFeatures( + lowered.module, + "llvm", + opts.dynamic ?? false, + opts.optimization ?? "release", + ), + ffi, + optimization: opts.optimization ?? "release", + }), + }, + }; + } + return { ok: true, artifact: { kind: outputKind, path: opts.outPath } }; + } + + await mkdir(opts.outDir, { recursive: true }); + const llvmPath = defaultSourcePaths.llvm; + const backend = "llvm" as const; + let llvmSource: string; + try { + llvmSource = emitLlvmModule(lowered.module, { + targetTriple: process.env["SCRIPTC_TARGET"] ?? "", + ...debugOptions, + pointerBits: buildPlatform === "wasi" ? 32 : 64, + wasi: buildPlatform === "wasi", + runtimeAbiMarker: opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, "llvm"), + }); + } catch (err) { + if (!(err instanceof LlvmUnsupportedError)) throw err; + return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; + } + await writeFile(llvmPath, llvmSource); + let irPath: string | undefined; + if (opts.emitIr) { + irPath = defaultSourcePaths.ir; + await writeFile(irPath, serializeModule(lowered.module)); + } + + const nativeFeatures = executableNativeFeatures( + lowered.module, + backend, + opts.dynamic ?? false, + opts.optimization ?? "release", + ); + const programSplit = + backend === "llvm" && (opts.optimization ?? "release") === "dev" && + !(opts.sanitize ?? false) && llvmSource !== null + ? splitLlvmProgram(llvmSource) + : null; + return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts }; +} + async function compileTracked( entryPath: string, opts: CompileRequestOptions, @@ -828,6 +931,10 @@ async function compileTracked( ): Promise { entryPath = resolve(entryPath); const outputKind = opts.outputKind ?? "exe"; + if ((opts.backend !== undefined && opts.backend !== "llvm") || + !["ir", "llvm", "asm", "obj", "exe"].includes(outputKind)) { + return { ok: false, diagnostics: [nativeCodegenDiag("SC3002", "LLVM is the only backend; supported outputs are ir, llvm, asm, obj, and exe", entryPath)], sourceTexts: new Map() }; + } if (opts.windowsSubsystem !== undefined && outputKind !== "exe") { return { ok: false, @@ -865,6 +972,10 @@ async function compileTracked( sourceTexts: new Map(), }; } + if (outputKind === "exe" && opts.sanitize !== true && process.env["SCRIPTC_FETCH_CURL"] !== "1") { + const refusal = nativeCodegenTargetRefusal(); + if (refusal !== null) return { ok: false, diagnostics: [nativeCodegenDiag("SC3002", refusal, entryPath)], sourceTexts: new Map() }; + } let ffi: FfiProfile | null = null; let ffiProfileBytes: Uint8Array | null = null; if (opts.ffiProfilePath !== undefined) { @@ -952,7 +1063,7 @@ async function compileTracked( diagnostics: [ targetRefusalDiag( mobileTarget, - "standalone executable builds — mobile targets produce library-mode static archives (SCRIPTC_CC=zigcc scriptc build --lib --profile ) for an embedding app to link", + "standalone executable builds — mobile targets produce library-mode static archives (scriptc build --lib --profile ) for an embedding app to link", entryLoc, ), ], @@ -976,7 +1087,7 @@ async function compileTracked( if (outputKind === "exe") { const implementation = await compilerImplementationIdentity(); const helperObjectRoute = opts.nativeProgramObject === true || - (opts.backend !== "c" && usesPrecompiledRuntimePack(opts, "llvm")); + (usesPrecompiledRuntimePack(opts, "llvm")); earlyCacheOptions = { entryPath, outDir: opts.outDir, @@ -984,7 +1095,7 @@ async function compileTracked( emitIr: opts.emitIr ?? false, sanitize: opts.sanitize ?? false, dynamic: opts.dynamic ?? false, - backend: opts.backend ?? "auto", + backend: "llvm", ...(opts.optimization === "dev" ? { optimization: "dev" as const } : {}), ...(opts.strip ? { strip: true as const } : {}), ...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}), @@ -1033,22 +1144,16 @@ async function compileTracked( artifact: { kind: "exe", path: opts.outPath, - translationUnitPath: earlyHit.cPath, + translationUnitPath: earlyHit.llvmPath, backend: earlyHit.native.backend, - ...(earlyHit.native.llvmRefusal === undefined - ? {} - : { llvmRefusal: earlyHit.native.llvmRefusal }), }, binaryPath: opts.outPath, - cPath: earlyHit.cPath, + llvmPath: earlyHit.llvmPath, backend: earlyHit.native.backend, ...(earlyHit.irPath === undefined ? {} : { irPath: earlyHit.irPath }), - ...(earlyHit.native.llvmRefusal === undefined - ? {} - : { llvmRefusal: earlyHit.native.llvmRefusal }), }; } - let nativeInputPath = earlyHit.cPath; + let nativeInputPath = earlyHit.llvmPath; let nativeProgramObject: { linkPath: string; artifactPath: string; @@ -1066,7 +1171,7 @@ async function compileTracked( nativeProgramObject = await emitNativeProgramObject( entryPath, opts, - await readFile(earlyHit.cPath, "utf8"), + await readFile(earlyHit.llvmPath, "utf8"), ); nativeInputPath = nativeProgramObject.linkPath; } catch (err) { @@ -1127,226 +1232,19 @@ async function compileTracked( artifact: { kind: "exe", path: opts.outPath, - translationUnitPath: earlyHit.cPath, + translationUnitPath: earlyHit.llvmPath, backend: earlyHit.native.backend, - ...(earlyHit.native.llvmRefusal === undefined - ? {} - : { llvmRefusal: earlyHit.native.llvmRefusal }), }, binaryPath: opts.outPath, - cPath: earlyHit.cPath, + llvmPath: earlyHit.llvmPath, backend: earlyHit.native.backend, ...(earlyHit.irPath === undefined ? {} : { irPath: earlyHit.irPath }), - ...(earlyHit.native.llvmRefusal === undefined - ? {} - : { llvmRefusal: earlyHit.native.llvmRefusal }), }; } - const fe = runFrontend(entryPath, loadProgram, opts.npmStatic); - let lowered: LowerResult; - let entryText: string; - let sourceTexts: Map; - // The frontend (and its tsgo server) is released as soon as lowering - // ends — clang and the link never hold it open. - try { - const fail = (diagnostics: ScrDiagnostic[]): CompileFailure => ({ - ok: false, - diagnostics, - sourceTexts: fe.sourceTexts(), - }); - - if (fe.preflight.length > 0) return fail(fe.preflight); - - try { - lowered = fe.lower({ - dynamic: opts.dynamic ?? false, - targetPlatform: buildPlatform, - ...(ffi !== null ? { ffiImports: ffi.functions } : {}), - }); - } catch (e) { - // The last-resort panic fence: an upstream tsgo panic that crossed a - // checker call no statement/collection fence wrapped still becomes a - // clean failed compile (anchored at the entry), never a crashed CLI. - if (!isCheckerPanic(e)) throw e; - return fail([ - checkerPanicDiag(e.message.split("\n", 1)[0]!, { file: entryPath, start: 0, end: 0 }), - ]); - } - if (lowered.module === null) return fail(lowered.diagnostics); - - const validation = validateModule(lowered.module); - if (validation.length > 0) { - return fail(validation.map((v) => iceDiag(v.message, v.loc))); - } - if (buildPlatform === "wasi") { - const entryLoc: SrcLoc = { file: entryPath, start: 0, end: 0 }; - if (opts.sanitize) { - return fail([targetRefusalDiag("wasm32-wasi", "--sanitize", entryLoc)]); - } - if (ffi !== null) { - return fail([targetRefusalDiag("wasm32-wasi", "native FFI manifests", entryLoc)]); - } - const unavailable = moduleWasiUnavailableSurface(lowered.module); - if (unavailable !== null) { - return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]); - } - if (opts.backend === "c" || outputKind === "c") { - const asyncSurface = moduleLibAsyncSurface(lowered.module); - if (asyncSurface !== null) { - return fail([ - backendRefusalDiag("c", "wasm32-wasi", asyncSurface.surface, asyncSurface.loc), - ]); - } - } - } - entryText = fe.entryText(); - sourceTexts = fe.sourceTexts(); - } finally { - fe.dispose(); - } - - const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); - const defaultSourcePaths = { - ir: join(opts.outDir, `${stem}.ir.json`), - c: join(opts.outDir, `${stem}.c`), - llvm: join(opts.outDir, `${stem}.ll`), - } as const; - const debugOptions = opts.optimization === "dev" && !opts.strip - ? { debugSources: sourceTexts } - : {}; - - if (outputKind === "ir") { - await mkdir(dirname(opts.outPath), { recursive: true }); - await writeFile(opts.outPath, serializeModule(lowered.module)); - return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; - } - - if (outputKind === "c") { - await mkdir(dirname(opts.outPath), { recursive: true }); - await writeFile(opts.outPath, emitCModuleChunks(lowered.module, entryText, debugOptions)); - return { ok: true, artifact: { kind: "c", path: opts.outPath } }; - } - - if (outputKind === "llvm" || outputKind === "asm" || outputKind === "obj") { - let llvm: string; - try { - llvm = emitLlvmModule(lowered.module, { - targetTriple: process.env["SCRIPTC_TARGET"] ?? "", - ...debugOptions, - pointerBits: buildPlatform === "wasi" ? 32 : 64, - wasi: buildPlatform === "wasi", - runtimeAbiMarker: outputKind === "obj", - }); - } catch (err) { - if (!(err instanceof LlvmUnsupportedError)) throw err; - return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; - } - if (outputKind === "llvm") { - await mkdir(dirname(opts.outPath), { recursive: true }); - await writeFile(opts.outPath, llvm); - } else { - try { - await emitNativeArtifact({ - outputPath: opts.outPath, - llvm, - outputKind, - sourcePath: entryPath, - optimization: opts.optimization === "dev" ? "0" : "2", - ...(opts.sanitize === undefined ? {} : { sanitize: opts.sanitize }), - }); - } catch (err) { - if (!(err instanceof NativeCodegenError)) throw err; - return { - ok: false, - diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], - sourceTexts, - }; - } - } - if (outputKind === "obj" && opts.nativeLinkInfo === true) { - const target = nativeCodegenTarget(); - if (target === null) { - throw new InternalCompilerError("native object emitted without a native target"); - } - return { - ok: true, - artifact: { - kind: "obj", - path: opts.outPath, - nativeLinkInfo: await createNativeLinkInfo({ - programObject: opts.outPath, - target, - features: executableNativeFeatures( - lowered.module, - "llvm", - opts.dynamic ?? false, - opts.optimization ?? "release", - ), - ffi, - optimization: opts.optimization ?? "release", - }), - }, - }; - } - return { ok: true, artifact: { kind: outputKind, path: opts.outPath } }; - } - - await mkdir(opts.outDir, { recursive: true }); - // Both backends hang off the same in-memory IrModule (never the JSON - // dump); the LLVM backend's .ll takes the .c's seat on the exact clang - // command line below — compileC accepts either. The default lane tries - // LLVM first; a tier refusal retries ONLY the emit with the C backend - // (the frontend ran once, the IR is backend-agnostic — nothing recompiles). - let cPath = defaultSourcePaths.c; - let backend: "c" | "llvm" = "c"; - let llvmSource: string | null = null; - let llvmRefusal: string | undefined; - if (opts.backend !== "c") { - try { - const ll = emitLlvmModule(lowered.module!, { - targetTriple: process.env["SCRIPTC_TARGET"] ?? "", - ...debugOptions, - pointerBits: buildPlatform === "wasi" ? 32 : 64, - wasi: buildPlatform === "wasi", - runtimeAbiMarker: - opts.nativeProgramObject === true || - usesPrecompiledRuntimePack(opts, "llvm"), - }); - cPath = defaultSourcePaths.llvm; - await writeFile(cPath, ll); - llvmSource = ll; - backend = "llvm"; - } catch (err) { - if (!(err instanceof LlvmUnsupportedError)) throw err; - // Explicit backend "llvm" keeps the fail-loudly contract (the - // debugging/CI pin): SC3001, never a silent lane change. - if (opts.backend === "llvm" || buildPlatform === "wasi") { - return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; - } - llvmRefusal = err.kind; - } - } - if (backend === "c") { - await writeFile(cPath, emitCModuleChunks(lowered.module!, entryText, debugOptions)); - } - let irPath: string | undefined; - if (opts.emitIr) { - irPath = defaultSourcePaths.ir; - await writeFile(irPath, serializeModule(lowered.module)); - } - - const nativeFeatures = executableNativeFeatures( - lowered.module, - backend, - opts.dynamic ?? false, - opts.optimization ?? "release", - llvmRefusal, - ); - const programSplit = - backend === "llvm" && (opts.optimization ?? "release") === "dev" && - !(opts.sanitize ?? false) && llvmSource !== null - ? splitLlvmProgram(llvmSource) - : null; + const prepared = await prepareExecutableInput(entryPath, opts, ffi, buildPlatform); + if ("ok" in prepared) return prepared; + const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared; + const backend = "llvm" as const; await mkdir(dirname(opts.outPath), { recursive: true }); if (earlyCacheOptions === null) { throw new InternalCompilerError("executable emission without executable cache options"); @@ -1378,7 +1276,7 @@ async function compileTracked( } await compileExecutableNative( nativeFeatures, - nativeProgramObject?.linkPath ?? cPath, + nativeProgramObject?.linkPath ?? llvmPath, opts.outPath, opts.sanitize ?? false, ffi, @@ -1388,7 +1286,7 @@ async function compileTracked( nativeProgramObject?.dependencies, opts.nativeProgramObject === true ? undefined : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - cPath, + llvmPath, native: nativeFeatures, executableRestored: true, nativeDependencies: dependencies, @@ -1425,7 +1323,7 @@ async function compileTracked( } if (!publishedExecutable) { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - cPath, + llvmPath, native: nativeFeatures, executableRestored: false, frontend: frontendInputs.snapshot(), @@ -1438,15 +1336,13 @@ async function compileTracked( artifact: { kind: "exe", path: opts.outPath, - translationUnitPath: cPath, + translationUnitPath: llvmPath, backend, - ...(llvmRefusal !== undefined ? { llvmRefusal } : {}), }, binaryPath: opts.outPath, - cPath, + llvmPath, backend, ...(irPath !== undefined ? { irPath } : {}), - ...(llvmRefusal !== undefined ? { llvmRefusal } : {}), }; } @@ -1472,7 +1368,7 @@ export type CompileLibraryResult = /** `sidecarPath` is present exactly when the profile declares a * `sidecar` section: the contract JSON written beside the archive by * the same invocation (ask 2). */ - | { ok: true; archivePath: string; cPath: string; backend: "c" | "llvm"; irPath?: string; sidecarPath?: string } + | { ok: true; archivePath: string; llvmPath: string; backend: "llvm"; irPath?: string; sidecarPath?: string } | { ok: false; diagnostics: ScrDiagnostic[]; sourceTexts: Map }; /** The marshalling-class fit over IR types (design §4.2 + the ratified @@ -1576,7 +1472,7 @@ function resolveLibrarySection( // the structured trap-teaching message: the profile's teaching for // SC4012 (or the mode's default text), the code, the trapping // export's C symbol exactly as the host linked it, and the - // profile's remediation when supplied — so both backends emit the + // profile's remediation when supplied — so the backend emits the // same bytes and the sink sees one canonical message. resolvedExport.inboundBytesTrap = assembleTrapTeaching( profileTeaching(profile, LIB_INBOUND_BYTES_TRAP_CODE) ?? @@ -1606,9 +1502,8 @@ function resolveLibrarySection( if (diagnostics.length > 0) return { diagnostics }; // The runtime detected-trap overlay rows: one per family code the profile // declares teaching or remediation text for, in the registry family's - // order. Both backends emit exactly these rows as the program TU's - // overlay table, so the funnel-assembled sink message is - // emission-invariant by construction. (SC4012 stays compile-time + // order. LLVM emits these rows as the program TU's overlay table, + // which the runtime uses to assemble the sink message. (SC4012 stays compile-time // assembled into the wrapper's message above and never reaches the // funnel's assembly path.) const trapOverlays: IrLibSection["trapOverlays"] = []; @@ -1634,7 +1529,7 @@ function resolveLibrarySection( threadInstances: profile.instancePerThread, // Host-callback channels: declaration order is the runtime slot // assignment, and the unregistered-call trap text is assembled HERE, - // once, so both backends emit identical constant bytes (a DETECTED + // once, for consistent constant bytes (a DETECTED // trap: the funnel classifies the "scriptc: library callback " // prefix as SC4025 and names the entry the host called — the entry // is runtime knowledge, so no compile-time SC4012-style assembly @@ -1852,20 +1747,21 @@ function mergeSidecarIntSlots( function libraryNativeFeatures( mod: IrModule, - backend: "c" | "llvm", + backend: "llvm", ): EarlyLibraryNativeFeatures { + const features = moduleRuntimeFeatures(mod); return { backend, - regex: moduleUsesRegex(mod), - assert: moduleUsesAssert(mod), - inspect: moduleUsesInspect(mod), - symbol: moduleUsesSymbol(mod), - bigint: moduleUsesBigInt(mod), - searchParams: moduleUsesSearchParams(mod), - emitter: moduleUsesEmitter(mod), - zlib: moduleUsesZlib(mod), - copying: moduleUsesCopying(mod), - textDecoderLegacy: moduleUsesLegacyTextDecoder(mod), + regex: features.regex, + assert: features.assert, + inspect: features.inspect, + symbol: features.symbol, + bigint: features.bigint, + searchParams: features.searchParams, + emitter: features.emitter, + zlib: features.zlib, + copying: features.copying, + textDecoderLegacy: features.legacyTextDecoder, ...(mod.lib?.identity !== undefined ? { buildId: mod.lib.identity.buildId } : {}), }; } @@ -1888,47 +1784,45 @@ function libraryLocalizeSymbols(profile: LibraryProfile): string[] | undefined { async function compileLibraryNative( profile: LibraryProfile, - cPath: string, + llvmPath: string, archivePath: string, sanitize: boolean, features: EarlyLibraryNativeFeatures, ): Promise { const localizeSymbols = libraryLocalizeSymbols(profile); - let identityCSource: string | undefined; + let identityLlvmSource: string | undefined; let programSource: string | undefined; if ( - profile.sidecar !== null || profile.emission === "c" || + profile.sidecar !== null || (profile.emission === "llvm" && profile.optimization === "dev" && !sanitize) ) { - const publicSource = await readFile(cPath, "utf8"); + const publicSource = await readFile(llvmPath, "utf8"); programSource = publicSource; } if (profile.sidecar !== null) { if (features.buildId === undefined) throw new InternalCompilerError("library identity TU has no build id"); - const withoutIdentity = stripLibraryIdentity(programSource!, profile.emission); + const withoutIdentity = stripLibraryIdentity(programSource!); if (withoutIdentity === programSource) { throw new InternalCompilerError("generated public library TU has no identity region"); } programSource = withoutIdentity; - identityCSource = [ - "#include ", - "#include ", - `uint64_t ${profile.sidecar.buildIdSymbol}(void) { return UINT64_C(0x${features.buildId}); }`, - `uint32_t ${profile.sidecar.abiVersionSymbol}(void) { return ${profile.sidecar.abiVersion}u; }`, - "", - ].join("\n"); - } - if (profile.emission === "c") { - programSource = stripLibrarySourceComments(programSource!, profile.entry); + identityLlvmSource = emitLibraryIdentityLines({ + buildIdSymbol: profile.sidecar.buildIdSymbol, + abiVersionSymbol: profile.sidecar.abiVersionSymbol, + buildId: features.buildId, + abiVersion: profile.sidecar.abiVersion, + }, "").join("\n"); } const llvmSplit = profile.emission === "llvm" && profile.optimization === "dev" && !sanitize && programSource !== undefined ? splitLlvmLibraryProgram(programSource) : null; - await compileExternalCLibrary({ - cPath, + const packTarget = !sanitize ? nativeCodegenTarget() : null; + if (!sanitize && packTarget === null) throw new NativeCodegenError("SC3002", nativeCodegenTargetRefusal() ?? "unsupported library target"); + const archiveOptions: Parameters[0] = { + cPath: llvmPath, ...(programSource !== undefined ? { programSource } : {}), - ...(identityCSource !== undefined ? { identityCSource } : {}), + ...(identityLlvmSource !== undefined ? { identityLlvmSource } : {}), ...(llvmSplit !== null ? { programShards: llvmSplit.shards, @@ -1951,7 +1845,9 @@ async function compileLibraryNative( zlib: features.zlib, copying: features.copying, textDecoderLegacy: features.textDecoderLegacy, - }); + }; + if (packTarget !== null) await compilePackedLibrary(archiveOptions, packTarget); + else await compileExternalCLibrary(archiveOptions); } async function emitSemanticLibraryHit( @@ -1990,26 +1886,12 @@ async function emitSemanticLibraryHit( } await mkdir(opts.outDir, { recursive: true }); const stem = basename(profile.entry).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); - const cPath = join(opts.outDir, `${stem}.lib.${profile.emission === "llvm" ? "ll" : "c"}`); + const llvmPath = join(opts.outDir, `${stem}.lib.ll`); let translationUnit = hit.translationUnit; if (profile.sidecar !== null) { - translationUnit = replaceLibraryIdentity(translationUnit, profile.emission, mod.lib!.identity!); - } - if (profile.emission === "llvm") { - await writeFile(cPath, translationUnit); - } else { - const previous = hit.previousSources.get(mod.sourceFile); - const current = hit.sourceTexts.get(mod.sourceFile); - if (previous === undefined || current === undefined) { - throw new InternalCompilerError("semantic library cache lost the entry source text"); - } - translationUnit = rebaseLibrarySourceComments( - translationUnit, - mod.sourceFile, - createSourceLineRebaser(mod.sourceFile, previous, current), - ); - await writeFile(cPath, translationUnit); + translationUnit = replaceLibraryIdentity(translationUnit, mod.lib!.identity!); } + await writeFile(llvmPath, translationUnit); timing("semantic-tu-restore", { output_bytes: Buffer.byteLength(translationUnit) }); let irPath: string | undefined; if (opts.emitIr) { @@ -2018,7 +1900,7 @@ async function emitSemanticLibraryHit( } await compileLibraryNative( profile, - cPath, + llvmPath, archivePath, opts.sanitize ?? false, hit.native, @@ -2032,7 +1914,7 @@ async function emitSemanticLibraryHit( await writeFile(sidecarPath, sidecarJson); } await publishEarlyLibraryCache(cacheRoot, cacheOptions, { - cPath, + llvmPath, native: hit.native, frontend: hit.frontend, semantic: { mod, sources: hit.sourceTexts }, @@ -2045,7 +1927,7 @@ async function emitSemanticLibraryHit( return { ok: true, archivePath, - cPath, + llvmPath, backend: profile.emission, ...(irPath !== undefined ? { irPath } : {}), ...(sidecarPath !== undefined ? { sidecarPath } : {}), @@ -2055,7 +1937,13 @@ async function emitSemanticLibraryHit( export async function compileLibrary(opts: CompileLibraryOptions): Promise { clearCompileSessionCaches(); const frontendInputs = new FrontendInputTracker(); - return frontendInputs.run(() => compileLibraryTracked(opts, frontendInputs)); + try { + return await frontendInputs.run(() => compileLibraryTracked(opts, frontendInputs)); + } catch (error) { + if (error instanceof RuntimePackError) return { ok: false, diagnostics: [runtimePackDiagnostic(error, opts.profilePath)], sourceTexts: new Map() }; + if (error instanceof NativeCodegenError) return { ok: false, diagnostics: [nativeCodegenDiag(error.diagnosticCode, error.message, opts.profilePath)], sourceTexts: new Map() }; + throw error; + } } async function compileLibraryTracked( @@ -2086,7 +1974,6 @@ async function compileLibraryTracked( } const profile = loadedProfile.profile; const entryPath = profile.entry; - const buildPlatform = buildTargetPlatform(); const profileDir = dirname(resolve(opts.profilePath)); for (let directory = dirname(entryPath); ; directory = dirname(directory)) { for (const name of ["tsconfig.json", "package.json"]) { @@ -2125,6 +2012,8 @@ async function compileLibraryTracked( } } + const buildPlatform = buildTargetPlatform(); + // Multi-instance library mode (abi.localize_runtime) localizes per // OBJECT FORMAT: ELF and COFF archives localize from any host (cross // ELF merges through the cross driver's own lld; COFF merges and @@ -2135,8 +2024,11 @@ async function compileLibraryTracked( // backend work, naming the pairing. WASI retains the general // library-mode refusal below. if (profile.localizeRuntime && buildPlatform !== "wasi") { - const driver = resolveCc(); - const platform = targetPlatform(driver); + const packTarget = nativeCodegenTarget(); + const driver = opts.sanitize ? resolveCc() : { + target: packTarget?.llvmTriple ?? process.env["SCRIPTC_TARGET"] ?? null, + }; + const platform = buildPlatform; const targetArch = driver.target?.split("-", 1)[0] ?? null; // Native Linux retains its host-binutils implementation. Cross ELF is // rebuilt in process and currently accepts the two verified ELF64, @@ -2162,7 +2054,7 @@ async function compileLibraryTracked( ok: false, diagnostics: decorateLibraryRefusals([ targetRefusalDiag( - driver.target ?? platform, + process.env["SCRIPTC_TARGET"] || driver.target || platform, subject, { file: entryPath, start: 0, end: 0 }, ), @@ -2197,7 +2089,7 @@ async function compileLibraryTracked( timing("early-cache-hit"); await compileLibraryNative( profile, - earlyHit.cPath, + earlyHit.llvmPath, archivePath, opts.sanitize ?? false, earlyHit.native, @@ -2207,7 +2099,7 @@ async function compileLibraryTracked( return { ok: true, archivePath, - cPath: earlyHit.cPath, + llvmPath: earlyHit.llvmPath, backend: earlyHit.native.backend, ...(earlyHit.irPath !== undefined ? { irPath: earlyHit.irPath } : {}), ...(earlyHit.sidecarPath !== undefined ? { sidecarPath: earlyHit.sidecarPath } : {}), @@ -2245,7 +2137,6 @@ async function compileLibraryTracked( source_files: fe.sourceTexts().size, }); let lowered: LowerResult; - let entryText: string; let sourceTexts: Map; let entryInfo: Map; let contractFacts: ContractFacts | null; @@ -2362,7 +2253,6 @@ async function compileLibraryTracked( } if (lowered.module === null) return fail(lowered.diagnostics); entryInfo = fe.entryExports(); - entryText = fe.entryText(); sourceTexts = fe.sourceTexts(); } finally { fe.dispose(); @@ -2462,22 +2352,15 @@ async function compileLibraryTracked( await mkdir(opts.outDir, { recursive: true }); const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, ""); - let cPath: string; - if (profile.emission === "llvm") { - try { - const ll = emitLlvmModule(mod, { targetTriple: process.env["SCRIPTC_TARGET"] ?? "" }); - timing("llvm-emit", { output_bytes: Buffer.byteLength(ll) }); - cPath = join(opts.outDir, `${stem}.lib.ll`); - await writeFile(cPath, ll); - timing("llvm-write"); - } catch (err) { - if (!(err instanceof LlvmUnsupportedError)) throw err; - // The profile PINS the emission — fail-loudly, never a lane change. - return fail([llvmRefusalDiag(err, entryPath)]); - } - } else { - cPath = join(opts.outDir, `${stem}.lib.c`); - await writeFile(cPath, emitCModule(mod, entryText)); + const llvmPath = join(opts.outDir, `${stem}.lib.ll`); + try { + const ll = emitLlvmModule(mod, { targetTriple: process.env["SCRIPTC_TARGET"] ?? "" }); + timing("llvm-emit", { output_bytes: Buffer.byteLength(ll) }); + await writeFile(llvmPath, ll); + timing("llvm-write"); + } catch (err) { + if (!(err instanceof LlvmUnsupportedError)) throw err; + return fail([llvmRefusalDiag(err, entryPath)]); } let irPath: string | undefined; @@ -2489,7 +2372,7 @@ async function compileLibraryTracked( const nativeFeatures = libraryNativeFeatures(mod, profile.emission); await compileLibraryNative( profile, - cPath, + llvmPath, archivePath, opts.sanitize ?? false, nativeFeatures, @@ -2508,7 +2391,7 @@ async function compileLibraryTracked( await writeFile(sidecarPath, sidecarJson); } const earlyPublish: EarlyLibraryCachePublish = { - cPath, + llvmPath, native: nativeFeatures, frontend: frontendInputs.snapshot(), semantic: { mod, sources: sourceTexts }, @@ -2522,7 +2405,7 @@ async function compileLibraryTracked( return { ok: true, archivePath, - cPath, + llvmPath, backend: profile.emission, ...(irPath !== undefined ? { irPath } : {}), ...(sidecarPath !== undefined ? { sidecarPath } : {}), diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 73d80b91c..7164bc0d6 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from "vitest"; +import { IR_VERSION } from "./serialize.js"; +import { moduleRuntimeFeatures, moduleUsesHttp2, moduleUsesTlsCa, VOID, type IrModule, type IrLibFn } from "./ir.js"; import { DYN, F64, HANDLE_KINDS, BYTES_ELEMENT_NAME, bytesOf, canConvertToDyn, canDynCheckTo, type IrBytesElem, isIslandCallbackParamType, isJsonSafeType, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, type IrType, type IrUnionDef, POINTER_KINDS, STRING, arrayOf, typeEquals, typeKey } from "./ir.js"; describe("IR kind sets", () => { @@ -120,3 +122,58 @@ describe("native bigint checked storage", () => { } }); }); + + +describe("runtime feature snapshots", () => { + const loc = { file: "features.ts", start: 0, end: 0 }; + const module = (): IrModule => ({ + irVersion: IR_VERSION, sourceFile: loc.file, entry: "main", + functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }], + }); + const call = (fn: IrLibFn) => ({ + kind: "exprStmt" as const, loc, + expr: { kind: "libCall" as const, fn, args: [], type: VOID, loc }, + }); + + test("updates link requirements after an IR edit without changing earlier snapshots", () => { + const mod = module(); + const first = moduleRuntimeFeatures(mod); + mod.functions[0]!.body.push(call("http2.connect"), call("tlsca.get")); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, http2: true, tls: true, tlsCa: true }); + expect(moduleUsesHttp2(mod)).toBe(true); + expect(moduleUsesTlsCa(mod)).toBe(true); + expect(first).toMatchObject({ net: false, http: false, http2: false, tls: false, tlsCa: false }); + mod.functions[0]!.body = []; + expect(moduleRuntimeFeatures(mod)).toEqual(first); + expect(moduleUsesHttp2(mod)).toBe(false); + }); + + test("keeps legacy HTTP/2 calls and isolated CA inspection out of the HTTP/2 unit", () => { + const mod = module(); + mod.functions[0]!.body.push(call("http2.streamNoop")); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, http2: false, tls: true }); + mod.functions[0]!.body = [call("tlsca.get")]; + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: false, http: false, http2: false, tls: false, tlsCa: true }); + }); + + test("retains runtime units needed only by nested handle storage", () => { + const mod = module(); + mod.functions[0]!.locals.push({ + id: "handles", name: "handles", mutable: false, + type: arrayOf({ kind: "promise", inner: { kind: "secureCtx" } }), + }); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, tls: true, http2: false }); + }); + + test("detects promise boxing independently of static promise storage", () => { + const mod = module(); + const promise = { kind: "promise" as const, inner: STRING }; + mod.functions[0]!.locals.push({ id: "promise", name: "promise", mutable: false, type: promise }); + expect(moduleRuntimeFeatures(mod).dynAsync).toBe(false); + mod.functions[0]!.body.push({ kind: "exprStmt", loc, expr: { + kind: "dynFrom", type: DYN, loc, + value: { kind: "varRef", localId: "promise", type: promise, loc }, + } }); + expect(moduleRuntimeFeatures(mod).dynAsync).toBe(true); + }); +}); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index aaaca6f86..f4ca98ba8 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -7,8 +7,8 @@ import { InternalCompilerError } from "../errors.js"; * - Every node carries `loc` (source span) and every expression carries its * computed `type` — backends never re-derive types. * - Structured control flow (statement tree), not basic blocks: the only - * current backend is C and there are no optimization passes yet. When a mid-end - * arrives, a structured→CFG lowering pass slots in after validation. + * LLVM backend lowers these into control flow. A future IR optimization + * pass can run after validation. * - The type union is deliberately written for extension (`dyn` shipped * with JSON, `promise` with async). All switches over IrType/IrExpr/ * IrStmt must have exhaustive `never` default arms so adding a member @@ -379,7 +379,7 @@ export type HandleKind = typeof HANDLE_KIND_LIST[number]; type HandleType = Extract; export const HANDLE_KINDS = irKindSet(HANDLE_KIND_LIST); -/** The IR kinds represented as pointers in both native backends. */ +/** The IR kinds represented as pointers in native code. */ const POINTER_KIND_LIST = [ "string", "bigint", @@ -538,8 +538,8 @@ export function isUnitType(t: IrType): boolean { return t.kind === "undefinedT" || t.kind === "nullT"; } -/** Element kinds with a real ScrArr storage/RC representation in BOTH - * backends. Array-producing lowerings can learn their result element from +/** Element kinds with a real ScrArr storage/RC representation. + * Array-producing lowerings can learn their result element from * a callback rather than through mapType's ordinary T[] gate, so they must * share this predicate instead of reconstructing an array around an * otherwise-valid standalone type (Date, opaque handles, ...). */ @@ -761,42 +761,40 @@ export function typeKey(t: IrType): string { } export function typeEquals(a: IrType, b: IrType): boolean { - if (a.kind === "array") return b.kind === "array" && typeEquals(a.elem, b.elem); - if (a.kind === "bytes") return b.kind === "bytes" && a.elem === b.elem; - if (a.kind === "map") { - return b.kind === "map" && typeEquals(a.key, b.key) && typeEquals(a.value, b.value); - } - if (a.kind === "set") return b.kind === "set" && typeEquals(a.elem, b.elem); - if (a.kind === "func") { - return ( - b.kind === "func" && - a.params.length === b.params.length && - (a.rest === true) === (b.rest === true) && - a.restAbi === b.restAbi && - a.argumentsAll === b.argumentsAll && - a.params.every((p, i) => typeEquals(p, b.params[i]!)) && - typeEquals(a.ret, b.ret) - ); - } - if (a.kind === "object") return b.kind === "object" && a.className === b.className; - if (a.kind === "classval") return b.kind === "classval" && a.className === b.className; - if (a.kind === "moduleNs") return b.kind === "moduleNs" && a.moduleId === b.moduleId; - // The frontend deduplicates shapes structurally (one shapeId per canonical - // field list), so id equality IS structural equality. - if (a.kind === "record") return b.kind === "record" && a.shapeId === b.shapeId; - // Unions are interned like shapes: one unionId per canonical arm list. - if (a.kind === "union") return b.kind === "union" && a.unionId === b.unionId; - if (a.kind === "promise") return b.kind === "promise" && typeEquals(a.inner, b.inner); - if (a.kind === "generator") { - return ( - b.kind === "generator" && - (a.async === true) === (b.async === true) && - typeEquals(a.yieldT, b.yieldT) && - typeEquals(a.retT, b.retT) && - typeEquals(a.nextT, b.nextT) - ); + // Dispatch once: a chain of negative narrowing checks repeatedly retags + // the remaining variants when this comparator runs in the native compiler. + switch (a.kind) { + case "array": return b.kind === "array" && typeEquals(a.elem, b.elem); + case "bytes": return b.kind === "bytes" && a.elem === b.elem; + case "map": return b.kind === "map" && typeEquals(a.key, b.key) && typeEquals(a.value, b.value); + case "set": return b.kind === "set" && typeEquals(a.elem, b.elem); + case "func": + return ( + b.kind === "func" && + a.params.length === b.params.length && + (a.rest === true) === (b.rest === true) && + a.restAbi === b.restAbi && + a.argumentsAll === b.argumentsAll && + a.params.every((p, i) => typeEquals(p, b.params[i]!)) && + typeEquals(a.ret, b.ret) + ); + case "object": return b.kind === "object" && a.className === b.className; + case "classval": return b.kind === "classval" && a.className === b.className; + case "moduleNs": return b.kind === "moduleNs" && a.moduleId === b.moduleId; + // Shapes and unions are interned, so their ids determine equality. + case "record": return b.kind === "record" && a.shapeId === b.shapeId; + case "union": return b.kind === "union" && a.unionId === b.unionId; + case "promise": return b.kind === "promise" && typeEquals(a.inner, b.inner); + case "generator": + return ( + b.kind === "generator" && + (a.async === true) === (b.async === true) && + typeEquals(a.yieldT, b.yieldT) && + typeEquals(a.retT, b.retT) && + typeEquals(a.nextT, b.nextT) + ); + default: return a.kind === b.kind; } - return a.kind === b.kind; } /** True for types whose values are heap-allocated and reference-counted. @@ -874,7 +872,7 @@ export interface IrModule { * and system-library inputs. Absent when the build has no FFI manifest. */ ffiImports?: IrFfiImport[]; /** LIBRARY mode: the profile's resolved export map plus the - * mode-provided symbol names, landed ON the IR so both backends emit the + * mode-provided symbol names, landed ON the IR so the backend emits the * external-linkage wrappers and entries from the same facts — the two * emissions stay conformance-identical by construction. Absent on every * executable build (the backends emit main() exactly as always). */ @@ -1057,7 +1055,7 @@ export interface IrLibTrapOverlay { /** One resolved host-callback channel (the profile's `callbacks` entry * landed on the IR): compiled call sites of the channel's ambient binding - * lower as ffiCall nodes carrying the channel name, and both backends emit + * lower as ffiCall nodes carrying the channel name, and the backend emits * the same dispatch — fetch the slot's registered pointer through * scr_library_cb_require (which delivers `unregisteredTrap` through the * library funnel when the host never registered), then the typed indirect @@ -1073,7 +1071,7 @@ export interface IrLibCallback { /** The unregistered-call trap text (a DETECTED trap: plain bytes the * library funnel classifies SC4025 and assembles with the current * entry's symbol — unlike the SC4012 wrapper traps, the entry is only - * known at runtime). Built once at export resolution so both backends + * known at runtime). Built once at export resolution so generated wrappers * emit identical constants by construction. */ unregisteredTrap: string; } @@ -5439,7 +5437,7 @@ export type IrExpr = * "function"` — true exactly for the checked-dynamic tree's function kind (boxed * closures); function values are truthy and answer FALSE to the * `"object"` test, JS-exact. */ - | { kind: "dynTest"; test: "bigint" | "string" | "number" | "boolean" | "undefined" | "null" | "nullish" | "bytes" | "buffer" | "object" | "array" | "truthy" | "error" | "function"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; type: IrType; loc: SrcLoc } + | { kind: "dynTest"; test: "promise" | "bigint" | "string" | "number" | "boolean" | "undefined" | "null" | "nullish" | "bytes" | "buffer" | "object" | "array" | "truthy" | "error" | "function"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; type: IrType; loc: SrcLoc } /** Keyed read on a dyn value — `pkg.name` / `pkg["k"]` / the * `pkg?.scripts` chain step on a JSON.parse result. `key` is * string-typed (a strLit for the dot form); `type` is always dyn. An @@ -5937,7 +5935,8 @@ export function isJsonStringifyDynamicType( /** The recursion shared by checked JSON conversion and stringification. * Ordinary record fields always admit undefined by dropping the key. Array - * and tuple slots admit it only for stringification, which writes null. */ + * and tuple slots admit it only for stringification, which writes null. + * Direct loops avoid allocating captured callbacks at each recursive edge. */ function isJsonSafeAt( t: IrType, getRecord: (shapeId: string) => IrRecordShape | undefined, @@ -5962,12 +5961,11 @@ function isJsonSafeAt( if (!shape) return false; if (recordTextCodecClass(shape) !== null) return false; // The recursive knot: answer true and let the rest of the graph - // decide (any unsafe constituent is found on its own path; a false - // short-circuits every `every` up the walk). + // decide (any unsafe constituent ends the traversal immediately). if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); - if (!shape.fields.every((f) => isJsonSafeAt(f.type, getRecord, getUnion, stringify, !shape.tuple || stringify, visiting, nativeFields))) { - return false; + for (const field of shape.fields) { + if (!isJsonSafeAt(field.type, getRecord, getUnion, stringify, !shape.tuple || stringify, visiting, nativeFields)) return false; } // Overflow values sit in record-key position too: dyn is JSON-safe // HERE (the checked-dynamic tree serializes itself; undefined-valued entries drop @@ -5984,7 +5982,10 @@ function isJsonSafeAt( const key = `${t.unionId}:${stringify}:${undefinedAllowed}`; if (visiting.has(key)) return true; // the recursive knot, union-flavored visiting.add(key); - return def.arms.every((a) => a.kind === "undefinedT" ? undefinedAllowed : isJsonSafeAt(a, getRecord, getUnion, stringify, undefinedAllowed, visiting, nativeFields)); + for (const arm of def.arms) { + if (!(arm.kind === "undefinedT" ? undefinedAllowed : isJsonSafeAt(arm, getRecord, getUnion, stringify, undefinedAllowed, visiting, nativeFields))) return false; + } + return true; } case "set": return nativeFields && !stringify && t.elem.kind === "dyn"; @@ -6342,7 +6343,9 @@ function canBoxDynComposite( // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); - if (!shape.fields.every((f) => canBoxDynComposite(f.type, getRecord, getUnion, visiting))) return false; + for (const field of shape.fields) { + if (!canBoxDynComposite(field.type, getRecord, getUnion, visiting)) return false; + } return !shape.indexValue || canBoxDynComposite(shape.indexValue, getRecord, getUnion, visiting); } case "union": { @@ -6350,7 +6353,10 @@ function canBoxDynComposite( if (!def) return false; if (visiting.has(t.unionId)) return true; visiting.add(t.unionId); - return def.arms.every((a) => canBoxDynComposite(a, getRecord, getUnion, visiting)); + for (const arm of def.arms) { + if (!canBoxDynComposite(arm, getRecord, getUnion, visiting)) return false; + } + return true; } default: return false; @@ -6502,6 +6508,156 @@ export function canExitIslandToType( return false; } +export interface RuntimeFeatures { + regex: boolean; + copying: boolean; + legacyTextDecoder: boolean; + fileHandle: boolean; + fetch: boolean; + processEvents: boolean; + emitter: boolean; + stream: boolean; + zlib: boolean; + dc: boolean; + assert: boolean; + dynInvoke: boolean; + dynAsync: boolean; + inspect: boolean; + childProcess: boolean; + net: boolean; + symbol: boolean; + bigint: boolean; + searchParams: boolean; + qs: boolean; + parseArgs: boolean; + fsWatch: boolean; + nodeTest: boolean; + dgram: boolean; + http: boolean; + http2: boolean; + tls: boolean; + tlsCa: boolean; +} + +const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + "async.awaitDyn", "timers.immediatePromise", + "process.onUncaughtException", "process.offUncaughtException", + "process.onUnhandledRejection", "process.offUnhandledRejection", + "process.onRejectionHandled", "process.offRejectionHandled", + "process.onWarning", "process.offWarning", "process.emitWarning", + "als.new", "als.get", "als.run", "als.exitRun", "als.enterWith", "als.disable", + "dc.chanBindStore", "dc.chanUnbindStore", "dc.chanRunStores", +]); + +/** Inspect types and expressions together in one traversal. This is a fresh + * snapshot: callers may mutate an IR module between compilation passes. */ +export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { + return scanRuntimeFeatures(mod); +} + +function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures { + const features: RuntimeFeatures = { + regex: false, copying: false, legacyTextDecoder: false, fileHandle: false, + fetch: mod.embedded?.modules.some((m) => m.usesFetch === true) ?? false, + processEvents: false, emitter: (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), + stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), + zlib: mod.embedded?.edges.some((e) => e.to === "node:zlib") ?? false, + dc: false, assert: false, dynInvoke: false, dynAsync: false, inspect: false, + childProcess: false, net: false, symbol: false, bigint: false, searchParams: false, + qs: false, parseArgs: false, fsWatch: false, nodeTest: false, dgram: false, + http: false, http2: false, tls: false, tlsCa: false, + }; + const visit = (value: unknown): void => { + if (value === null || typeof value !== "object" || (stopAt !== undefined && features[stopAt])) return; + if (Array.isArray(value)) { + for (const item of value) visit(item); + return; + } + const node = value as { + kind?: unknown; fn?: unknown; method?: unknown; op?: unknown; name?: unknown; + type?: { kind?: unknown }; + value?: { type?: { kind?: unknown; ret?: { kind?: unknown } } }; + }; + const kind = node.kind; + if (kind === "libCall" && typeof node.fn === "string") { + const fn = node.fn; + if (fn === "regexp.escape" || fn === "dyn.nativeRegexIs") features.regex = true; + if (fn === "text.decodeLegacy") features.legacyTextDecoder = true; + if (fn.startsWith("fetch.")) features.fetch = true; + if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; + if (fn.startsWith("emitter.")) features.emitter = true; + if (fn.startsWith("readable.") || fn.startsWith("writable.") || fn.startsWith("duplex.") || + fn.startsWith("transform.") || fn.startsWith("passthrough.") || fn.startsWith("sc.") || + fn.startsWith("stream.set") || fn === "stream.destroy" || fn === "stream.destroyErr" || + fn === "stream.prop" || fn === "stream.errored" || fn === "sp.finished" || fn === "sp.pipeline") features.stream = true; + if (fn.startsWith("zlib.")) features.zlib = true; + if (fn.startsWith("dc.")) features.dc = true; + if (fn.startsWith("assert.")) features.assert = true; + if (fn === "dyn.defineProps" || fn === "dyn.defineProperty" || fn === "dyn.objCreateWithProperties" || fn === "dyn.arrayProtoCall") features.dynInvoke = true; + if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; + if (fn.startsWith("insp.")) features.inspect = true; + if (fn.startsWith("cp.") || fn.startsWith("child.") || fn.startsWith("writer.") || fn.startsWith("spawnRes.") || + fn === "process.forkTarget" || fn === "process.connected" || fn === "process.send" || fn === "process.sendCb" || + fn === "process.disconnect" || fn === "process.onMessage" || fn === "process.onDisconnect") features.childProcess = true; + if (fn.startsWith("net.")) features.net = true; + if (fn.startsWith("http.")) { features.net = true; features.http = true; } + if (fn.startsWith("tls.") || fn.startsWith("https.") || fn.startsWith("http2.")) { + features.net = true; features.http = true; features.tls = true; + } + if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true; + if (fn.startsWith("sym.")) features.symbol = true; + if (fn.startsWith("bigint.")) features.bigint = true; + if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true; + if (fn === "qs.parse" || fn === "qs.stringify" || fn === "qs.unescape") features.qs = true; + if (fn === "util.parseArgs") features.parseArgs = true; + if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true; + if (fn.startsWith("test.")) features.nodeTest = true; + if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; + if (fn.startsWith("tlsca.")) features.tlsCa = true; + } else { + switch (kind) { + case "regex": case "regexLit": case "regexIntrinsic": features.regex = true; break; + case "strIntrinsic": + if (node.method === "toLowerCase" || node.method === "toUpperCase") features.regex = true; + break; + case "arrIntrinsic": + if (node.method === "toReversed" || node.method === "toSpliced" || node.method === "with" || node.method === "withUndefined") features.copying = true; + break; + case "bytesIntrinsic": + if (node.method === "toReversed" || node.method === "with" || node.method === "join" || node.method === "toArray") features.copying = true; + break; + case "fileHandle": features.fileHandle = true; break; + case "jsOp": + if (node.op === "globalGet" && node.name === "fetch") features.fetch = true; + break; + case "dynInvoke": features.dynInvoke = true; break; + case "awaitExpr": + if (node.type?.kind === "dyn") features.dynAsync = true; + break; + case "dynFrom": { + const boxed = node.value?.type; + if (boxed?.kind === "promise" || (boxed?.kind === "func" && boxed.ret?.kind === "promise")) features.dynAsync = true; + break; + } + case "child": case "childStream": case "childWriter": case "spawnRes": features.childProcess = true; break; + case "netServer": case "netSocket": features.net = true; break; + case "http2Session": case "http2Stream": features.net = true; features.http2 = true; break; + case "httpReq": case "httpRes": case "httpClientReq": features.net = true; features.http = true; break; + case "secureCtx": features.net = true; features.http = true; features.tls = true; break; + case "symbol": features.symbol = true; break; + case "bigint": features.bigint = true; break; + case "searchParams": features.searchParams = true; break; + case "fsWatcher": features.fsWatch = true; break; + case "testCtx": features.nodeTest = true; break; + case "dgramSocket": features.dgram = true; break; + } + } + for (const key of Object.keys(value)) visit((value as Record)[key]); + }; + visit(mod); + return features; +} + /** True when the module contains any regex construct — a regexLit / * regexIntrinsic node or a regex-typed slot anywhere. This is the link * switch that pulls scr_regex.c + the vendored libregexp into the binary @@ -6509,118 +6665,28 @@ export function canExitIslandToType( * JSON walk: `kind` discriminants live only on IR objects, so user string * VALUES can never false-positive. */ export function moduleUsesRegex(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const kind = (v as { kind?: unknown }).kind; - if (kind === "regex" || kind === "regexLit" || kind === "regexIntrinsic") { - found = true; - return; - } - // The lre-backed case conversions live in scr_regex.c (libunicode's - // case tables): they ride the same link switch as regex nodes. - if (kind === "strIntrinsic") { - const method = (v as { method?: unknown }).method; - if (method === "toLowerCase" || method === "toUpperCase") { - found = true; - return; - } - } - // RegExp.escape lives in scr_regex.c too (needing no engine — it - // keeps the always-linked string TU out of hello-world's size class). - if (kind === "libCall" && ["regexp.escape", "dyn.nativeRegexIs"].includes(String((v as { fn?: unknown }).fn))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "regex").regex; } /** True when the module contains an intrinsic whose implementation lives * in scr_copying.c. This is the link switch that keeps the optional * Array-copying and typed-array bridge TU out of unrelated binaries. */ export function moduleUsesCopying(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; method?: unknown }; - if ( - node.kind === "arrIntrinsic" && - (node.method === "toReversed" || - node.method === "toSpliced" || - node.method === "with" || - node.method === "withUndefined") - ) { - found = true; - return; - } - if ( - node.kind === "bytesIntrinsic" && - (node.method === "toReversed" || - node.method === "with" || - node.method === "join" || - node.method === "toArray") - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "copying").copying; } /** True when a non-UTF-8 TextDecoder call survives lowering. This gates the * generated legacy mapping tables inside scr_bytes.c; the default UTF-8 * decoder and unrelated Buffer users keep their prior runtime object. */ export function moduleUsesLegacyTextDecoder(mod: IrModule): boolean { - let found = false; - const visit = (value: unknown): void => { - if (found || value === null || typeof value !== "object") return; - if (Array.isArray(value)) { - for (const item of value) visit(item); - return; - } - const node = value as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && node.fn === "text.decodeLegacy") { - found = true; - return; - } - for (const key of Object.keys(value)) visit((value as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "legacyTextDecoder").legacyTextDecoder; } /** True when a FileHandle type survives in the module. This is the link * switch for scr_file_handle.c; fs/promises.open carries the type inside its * promise result even when no handle method is called. */ export function moduleUsesFileHandle(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - if ((v as { kind?: unknown }).kind === "fileHandle") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fileHandle").fileHandle; } /** True when user code lowers static fetch or the embedded npm graph @@ -6631,29 +6697,7 @@ export function moduleUsesFileHandle(mod: IrModule): boolean { * and local `fetch` bindings do not change the link or target capability. * Fetch-free graphs keep their exact historical link lines. */ export function moduleUsesFetch(mod: IrModule): boolean { - const embedded = mod.embedded; - if (embedded && embedded.modules.some((m) => m.usesFetch === true)) return true; - // User-code fetch is either the engine global or the static libCall pair. - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; op?: unknown; name?: unknown; fn?: unknown }; - if ( - (node.kind === "jsOp" && node.op === "globalGet" && node.name === "fetch") || - (node.kind === "libCall" && - typeof node.fn === "string" && node.fn.startsWith("fetch.")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fetch").fetch; } /** The libCall fns served by the OPTIONAL events unit (scr_events.c): @@ -6682,22 +6726,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ * gating precedent). Event-free programs pay zero bytes and keep their * exact link line. Same generic-walk shape as moduleUsesRegex. */ export function moduleUsesProcessEvents(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && PROCESS_EVENT_LIB_FNS.has(node.fn)) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "processEvents").processEvents; } /** True when the module uses the node:events EventEmitter surface — the @@ -6710,23 +6739,7 @@ export function moduleUsesProcessEvents(mod: IrModule): boolean { * no emitter-typed value). Emitter-free programs pay zero bytes and keep * their exact link line. */ export function moduleUsesEmitter(mod: IrModule): boolean { - if ((mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS)) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("emitter.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "emitter").emitter; } /** True when the program touches the node:stream surface (scr_stream.c — @@ -6737,34 +6750,7 @@ export function moduleUsesEmitter(mod: IrModule): boolean { * moduleUsesEmitter answers true whenever this does. Stream-free * programs pay zero bytes and keep their exact link line. */ export function moduleUsesStream(mod: IrModule): boolean { - if ((mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name))) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "libCall" && - typeof node.fn === "string" && - (node.fn.startsWith("readable.") || node.fn.startsWith("writable.") || - node.fn.startsWith("duplex.") || node.fn.startsWith("transform.") || - node.fn.startsWith("passthrough.") || - node.fn === "stream.destroy" || node.fn === "stream.destroyErr" || - node.fn === "stream.prop" || node.fn === "stream.errored" || - node.fn === "sp.finished" || node.fn === "sp.pipeline" || - node.fn.startsWith("sc.") || - node.fn.startsWith("stream.set")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "stream").stream; } /** True when the embedded npm graph has an edge into `builtin` — the @@ -6775,7 +6761,7 @@ export function moduleEmbedsBuiltin(mod: IrModule, builtin: string): boolean { } /** Embedded module texts at least this long are DEFLATE-compressed into - * the emitted C (island.ts; each stays plain when deflate does not + * the emitted LLVM (island.ts; each stays plain when deflate does not * shrink it) and inflated lazily by the island's module loader at first * load. Below it the zlib round trip cannot pay for itself. */ export const NPM_COMPRESS_MIN = 1024; @@ -6801,23 +6787,7 @@ export function moduleEmbedsCompressedNpm(mod: IrModule): boolean { * generic-walk shape as moduleUsesRegex: `kind`/`fn` discriminants live * only on IR objects. */ export function moduleUsesZlib(mod: IrModule): boolean { - if (moduleEmbedsBuiltin(mod, "node:zlib")) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("zlib.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "zlib").zlib; } /** True when the module contains any dc.* libCall — the link switch that @@ -6825,22 +6795,7 @@ export function moduleUsesZlib(mod: IrModule): boolean { * binary (native-toolchain.ts). Channel-free binaries keep their exact size class. * Same walk shape as moduleUsesZlib. */ export function moduleUsesDc(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("dc.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dc").dc; } /** True when the module contains any assert libCall — the link switch @@ -6850,22 +6805,7 @@ export function moduleUsesDc(mod: IrModule): boolean { * the historical command line and size. Same walk shape as * moduleUsesZlib. */ export function moduleUsesAssert(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("assert.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "assert").assert; } /** True when the module contains any dynInvoke node or dyn.defineProps @@ -6875,25 +6815,7 @@ export function moduleUsesAssert(mod: IrModule): boolean { * precedent — dispatch-free binaries keep their exact size class). Same * walk shape as moduleUsesZlib. */ export function moduleUsesDynInvoke(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "dynInvoke" || (node.kind === "libCall" && - (node.fn === "dyn.defineProps" || node.fn === "dyn.defineProperty" || - node.fn === "dyn.objCreateWithProperties" || - node.fn === "dyn.arrayProtoCall"))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dynInvoke").dynInvoke; } /** True when the module contains any insp libCall — the link switch that @@ -6907,92 +6829,18 @@ export function moduleUsesDynInvoke(mod: IrModule): boolean { * dynInvoke and dc gates (their TUs call into this one) — native-toolchain.ts. Same * walk shape as moduleUsesZlib. */ export function moduleUsesDynAsync(mod: IrModule): boolean { - const fns = new Set([ - "async.awaitDyn", "timers.immediatePromise", - "process.onUncaughtException", "process.offUncaughtException", - "process.onUnhandledRejection", "process.offUnhandledRejection", - "process.onRejectionHandled", "process.offRejectionHandled", - "process.onWarning", "process.offWarning", "process.emitWarning", - "als.new", "als.get", "als.run", "als.exitRun", "als.enterWith", "als.disable", - "dc.chanBindStore", "dc.chanUnbindStore", "dc.chanRunStores", - ]); - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown; type?: { kind?: unknown } }; - if (node.kind === "libCall" && typeof node.fn === "string" && fns.has(node.fn)) { - found = true; - return; - } - // A DYN-typed await reads through scr_await_dyn (the checked-dynamic tree-crossing - // await lives in the gated TU) — promise receivers' awaits and - // the lifted then/catch helpers alike. - const boxed = (node as { value?: { type?: { kind?: unknown; ret?: { kind?: unknown } } } }).value?.type; - if ((node.kind === "awaitExpr" && node.type?.kind === "dyn") || - (node.kind === "dynFrom" && (boxed?.kind === "promise" || (boxed?.kind === "func" && boxed.ret?.kind === "promise")))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dynAsync").dynAsync; } export function moduleUsesInspect(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("insp.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "inspect").inspect; } /** True when the module reaches child_process or carries one of its runtime * handle/result types. Besides the existing link consequence, this gates the * checked-dynamic ChildProcess handle table installed from scr_child.c. */ export function moduleUsesChildProcess(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "libCall" && - typeof node.fn === "string" && - (node.fn.startsWith("cp.") || node.fn.startsWith("child.") || node.fn.startsWith("writer.") || node.fn.startsWith("spawnRes.") || - node.fn === "process.forkTarget" || node.fn === "process.connected" || node.fn === "process.send" || - node.fn === "process.sendCb" || node.fn === "process.disconnect" || node.fn === "process.onMessage" || - node.fn === "process.onDisconnect") - ) { - found = true; - return; - } - if (node.kind === "child" || node.kind === "childStream" || node.kind === "childWriter" || node.kind === "spawnRes") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "childProcess").childProcess; } /** True when the module contains any net libCall — the link switch that @@ -7001,39 +6849,7 @@ export function moduleUsesChildProcess(mod: IrModule): boolean { * Net-free programs pay zero bytes and keep their exact link line. Same * generic-walk shape as moduleUsesZlib. */ export function moduleUsesNet(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("net.") || node.fn.startsWith("http.") || - node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - // http/tls/https/http2 all ride ON net: their libCalls need scr_net.c - // linked and installed too, so the net switch answers for every - // server-family prefix. - found = true; - return; - } - // A net-family HANDLE TYPE anywhere in the IR (a global or local whose - // initializing statement compiled to a runtime fence still carries the - // type): its emitted release call needs the unit linked even when no - // libCall survived the fencing. - if (node.kind === "netServer" || node.kind === "netSocket" || - node.kind === "http2Session" || node.kind === "http2Stream" || - node.kind === "httpReq" || node.kind === "httpRes" || - node.kind === "httpClientReq" || node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "net").net; } /** True when the module contains any sym.* libCall or a symbol-kind type @@ -7044,51 +6860,14 @@ export function moduleUsesNet(mod: IrModule): boolean { * fence still emits release calls that need the unit linked. Symbol-free * programs pay zero bytes and keep their exact link line. */ export function moduleUsesSymbol(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("sym.")) { - found = true; - return; - } - if (node.kind === "symbol") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "symbol").symbol; } /** True when bigint runtime operations or bigint-typed storage appears in * the IR. The type check keeps retain/release references link-safe even * when a producing statement was replaced by a runtime fence. */ export function moduleUsesBigInt(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "bigint" || - (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("bigint.")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "bigint").bigint; } /** True when the module uses the URLSearchParams surface — sp.* libCalls, @@ -7100,27 +6879,7 @@ export function moduleUsesBigInt(mod: IrModule): boolean { * programs keep their exact link line; scr_url.c itself stays * always-linked and never references the unit. */ export function moduleUsesSearchParams(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("sp.") || node.fn === "url.searchParams")) { - found = true; - return; - } - if (node.kind === "searchParams") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "searchParams").searchParams; } /** True when the module uses the node:querystring surface — the qs.* @@ -7131,45 +6890,14 @@ export function moduleUsesSearchParams(mod: IrModule): boolean { * loop hooks, cross-compiles everywhere). qs-free programs keep their * exact link line. Same generic-walk shape as moduleUsesZlib. */ export function moduleUsesQs(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn === "qs.parse" || node.fn === "qs.stringify" || node.fn === "qs.unescape")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "qs").qs; } /** True when the module uses native util.parseArgs — the link switch for * scr_util.c. The implementation is a pure checked-dynamic data transform, * cross-platform and independent of the island's node:util shim. */ export function moduleUsesParseArgs(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && node.fn === "util.parseArgs") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "parseArgs").parseArgs; } /** True when the module contains any fs.watch/watcher.* libCall — the @@ -7178,29 +6906,7 @@ export function moduleUsesParseArgs(mod: IrModule): boolean { * precedent). Watch-free programs pay zero bytes and keep their exact * link line. Same generic-walk shape as moduleUsesZlib. */ export function moduleUsesFsWatch(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("fs.watch") || node.fn.startsWith("watcher."))) { - found = true; - return; - } - // A watcher HANDLE TYPE left behind by a fenced statement still emits - // scr_watcher_release — the unit must link. - if (node.kind === "fsWatcher") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fsWatch").fsWatch; } /** True when the module contains any test.* libCall or a testCtx handle @@ -7209,28 +6915,7 @@ export function moduleUsesFsWatch(mod: IrModule): boolean { * (native-toolchain.ts + emitter; the moduleUsesDgram shape). Test-free programs pay * zero bytes and keep their exact link line. */ export function moduleUsesNodeTest(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("test.")) { - found = true; - return; - } - // A TestContext HANDLE TYPE left behind by a fenced statement still - // emits a release call — the unit must link (the dgram type story). - if (node.kind === "testCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "nodeTest").nodeTest; } /** True when the module contains any dgram.* or dns.* libCall — the @@ -7240,64 +6925,14 @@ export function moduleUsesNodeTest(mod: IrModule): boolean { * answers). Dgram-free programs pay zero bytes and keep their exact link * line. Same generic-walk shape as moduleUsesZlib. */ export function moduleUsesDgram(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("dgram.") || node.fn.startsWith("dns."))) { - found = true; - return; - } - // A dgram HANDLE TYPE left behind by a fenced statement still emits a - // release call — the unit must link (the moduleUsesNet type story). - if (node.kind === "dgramSocket") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dgram").dgram; } /** True when the module contains any http.* libCall — the link switch * that pulls scr_http.c into the binary (native-toolchain.ts; moduleUsesNet already * answers true for these, so scr_net.c comes along). */ export function moduleUsesHttpServer(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("http.") || node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - // scr_tls.c calls into scr_http.c unconditionally (the https server - // and client are the http ones over a transport), so any tls/https - // use pulls the http unit too. - found = true; - return; - } - // An http-family HANDLE TYPE left behind by a fenced statement still - // emits its release call — the unit must link (the moduleUsesNet type - // story; secureCtx rides here because scr_tls.c calls into scr_http.c). - if (node.kind === "httpReq" || node.kind === "httpRes" || - node.kind === "httpClientReq" || node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "http").http; } /** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the @@ -7311,27 +6946,7 @@ const HTTP2_LEGACY_FNS = new Set([ * http2.* libCall, or an h2 handle type left behind by a fenced statement * (its emitted release call needs the unit — the moduleUsesNet story). */ export function moduleUsesHttp2(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn)) { - found = true; - return; - } - if (node.kind === "http2Session" || node.kind === "http2Stream") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "http2").http2; } /** True when the module contains any tls.* or https.* libCall — the link @@ -7340,30 +6955,7 @@ export function moduleUsesHttp2(mod: IrModule): boolean { * true for these, so scr_net.c and scr_http.c come along). TLS-free * programs keep their exact link line and never build mbedTLS. */ export function moduleUsesTls(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - found = true; - return; - } - // A secureCtx HANDLE TYPE left behind by a fenced statement still - // emits scr_secure_ctx_release — the unit must link. - if (node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "tls").tls; } /** True when the module contains any tlsca.* libCall — the link switch @@ -7374,22 +6966,7 @@ export function moduleUsesTls(mod: IrModule): boolean { * also compiles the unit whenever TLS itself links — scr_tls.c consults * the unit's default-set override for its trust anchors. */ export function moduleUsesTlsCa(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("tlsca.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "tlsCa").tlsCa; } /* ── library mode's async_free gate ────────────────────────────────────── diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 5dad08455..f8c5d1876 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -142,6 +142,23 @@ test("logical trees retain left/right/parent diagnostic order", () => { ]); }); +test("conditional trees retain condition/then/else/parent diagnostic order", () => { + const at = (start: number) => ({ ...loc, start }); + const expr: IrExpr = { + kind: "ternary", type: STRING, loc: at(3), + cond: { kind: "numLit", value: 0, type: BOOL, loc: at(0) }, + then: { kind: "boolLit", value: true, type: F64, loc: at(1) }, + else_: { kind: "strLit", value: "wrong", type: BOOL, loc: at(2) }, + }; + expect(validateModule(expressionModule(expr, [])).map((error) => [error.loc.start, error.message])).toEqual([ + [0, "in main: numLit must be f64"], + [1, "in main: boolLit must be bool"], + [2, "in main: strLit must be string"], + [1, "in main: ternary then-branch: expected string, got f64"], + [2, "in main: ternary else-branch: expected string, got bool"], + ]); +}); + test.each(["callValue", "dynCall"] as const)("%s requires a checked-value receiver and preserves it in serialization", (kind) => { const funcType: IrType = { kind: "func", params: [], ret: DYN }; const closure: IrExpr = { kind: "closure", fnName: "callback", captures: [], type: funcType, loc }; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 474773feb..66fb454d1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -2244,37 +2244,30 @@ function validateFunction( } } - function checkNullishChain(e: IrExpr & { kind: "nullish" }): void { - // Preserve the recursive validator's left/right/parent diagnostic order - // without keeping its large native expression frame for every operand. - const parents: (IrExpr & { kind: "nullish" })[] = []; - let left: IrExpr = e; - while (left.kind === "nullish") { - parents.push(left); - left = left.left; - } - checkExpr(left); - for (let i = parents.length - 1; i >= 0; i--) { - const parent = parents[i]!; - checkExpr(parent.right); - checkNullishOperands(parent); - } - } - - function checkLogicalTree(root: IrExpr & { kind: "logical" }): void { - // Long predicates can associate in either direction. Keep their - // left/right/parent diagnostic order with bounded native stack use. + function checkBranchingTree(root: IrExpr): void { + // Predicates and conditional dispatch can nest in either direction. + // Preserve child-before-parent diagnostics with bounded native stack + // use, including trees that mix the three branching operators. const pending: { expr: IrExpr; visited: boolean }[] = [{ expr: root, visited: false }]; while (pending.length !== 0) { const task = pending.pop()!; const e = task.expr; - if (e.kind !== "logical") { - checkExpr(e); - } else if (!task.visited) { - pending.push({ expr: e, visited: true }); - pending.push({ expr: e.right, visited: false }); - pending.push({ expr: e.left, visited: false }); - } else { + if (!task.visited) { + if (e.kind === "logical" || e.kind === "nullish") { + pending.push({ expr: e, visited: true }); + pending.push({ expr: e.right, visited: false }); + pending.push({ expr: e.left, visited: false }); + } else if (e.kind === "ternary") { + pending.push({ expr: e, visited: true }); + pending.push({ expr: e.else_, visited: false }); + pending.push({ expr: e.then, visited: false }); + pending.push({ expr: e.cond, visited: false }); + } else { + checkExpr(e); + } + continue; + } + if (e.kind === "logical") { if ( e.type.kind !== "f64" && e.type.kind !== "string" && e.type.kind !== "bool" && e.type.kind !== "jsval" && e.type.kind !== "union" && e.type.kind !== "dyn" @@ -2284,6 +2277,13 @@ function validateFunction( if (e.type.kind === "union") checkTruthyUnion(e.type.unionId, e.loc); expectType(e.left, e.type, `logical ${e.op} left`); expectType(e.right, e.type, `logical ${e.op} right`); + } else if (e.kind === "nullish") { + checkNullishOperands(e); + } else if (e.kind === "ternary") { + expectType(e.cond, BOOL, "ternary condition"); + expectType(e.then, e.type, "ternary then-branch"); + expectType(e.else_, e.type, "ternary else-branch"); + if (e.type.kind === "void") err("ternary must not be void", e.loc); } } } @@ -2292,8 +2292,8 @@ function validateFunction( switch (e.kind) { case "numLit": // ±Infinity and NaN are real literals (the globals - // `Infinity`/`NaN`, Number constants) — both backends spell them - // (INFINITY/NAN macros in C, bit-encoded f64 in LLVM). + // `Infinity`/`NaN`, Number constants) — the backend spells them + // (bit-encoded f64 in LLVM). // procStream is the ONE non-f64 numLit: process.stdout/stderr as // first-class values mint the stream's fd (1/2) as the scalar — // the prefixStream idiom. @@ -2501,7 +2501,9 @@ function validateFunction( if (e.type.kind !== "bool") err("toBool must be bool", e.loc); break; case "logical": - checkLogicalTree(e); + case "nullish": + case "ternary": + checkBranchingTree(e); break; case "unionEq": { checkExpr(e.left); @@ -2545,15 +2547,6 @@ function validateFunction( expectType(e.right, STRING, `${e.kind} right`); if (e.type.kind !== "bool") err(`${e.kind} must be bool`, e.loc); break; - case "ternary": - checkExpr(e.cond); - checkExpr(e.then); - checkExpr(e.else_); - expectType(e.cond, BOOL, "ternary condition"); - expectType(e.then, e.type, "ternary then-branch"); - expectType(e.else_, e.type, "ternary else-branch"); - if (e.type.kind === "void") err("ternary must not be void", e.loc); - break; case "optChain": { checkExpr(e.receiver); // An island-handle chain (`x?.y` on 'any'): the nullish test is a @@ -2638,9 +2631,6 @@ function validateFunction( expectType(e, bound, "chainRecv"); break; } - case "nullish": - checkNullishChain(e); - break; case "orDefault": { checkExpr(e.left); checkExpr(e.right); diff --git a/packages/compiler/src/library/cache-primitives.ts b/packages/compiler/src/library/cache-primitives.ts index 65ee71f64..c38b59f3b 100644 --- a/packages/compiler/src/library/cache-primitives.ts +++ b/packages/compiler/src/library/cache-primitives.ts @@ -4,7 +4,7 @@ import { basename, dirname, join, resolve } from "node:path"; import type { FrontendInputExclusions } from "../frontend/input-tracker.js"; import { compilerReleaseVersion } from "./sidecar.js"; -export type CacheBackend = "c" | "llvm"; +export type CacheBackend = "llvm"; interface CacheOutputOptions { entryPath: string; @@ -13,8 +13,7 @@ interface CacheOutputOptions { } export interface CacheOutputPaths { - cPath: string; - alternateCPath: string; + llvmPath: string; irPath: string; } @@ -54,8 +53,7 @@ export function outputPaths( ): CacheOutputPaths { const stem = basename(options.entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "") + stemSuffix; return { - cPath: join(options.outDir, `${stem}.${backend === "llvm" ? "ll" : "c"}`), - alternateCPath: join(options.outDir, `${stem}.${backend === "llvm" ? "c" : "ll"}`), + llvmPath: join(options.outDir, `${stem}.ll`), irPath: join(options.outDir, `${stem}.ir.json`), }; } @@ -68,8 +66,7 @@ export function frontendOutputExclusions( ): FrontendInputExclusions { const paths = outputPaths(options, backend, stemSuffix); const outputArtifacts = [ - paths.cPath, - paths.alternateCPath, + paths.llvmPath, paths.irPath, ...additionalPaths, ].map((path) => resolve(path)); @@ -114,7 +111,7 @@ export function validNativeFeatures( ): value is T { if (value === null || typeof value !== "object") return false; const native = value as Partial; - return (native.backend === "c" || native.backend === "llvm") && + return native.backend === "llvm" && booleanKeys.every((key) => typeof native[key] === "boolean") && validAdditional(native); } diff --git a/packages/compiler/src/library/cache-reset-fixtures/profile.json b/packages/compiler/src/library/cache-reset-fixtures/profile.json index d482d2c2f..6e5b97752 100644 --- a/packages/compiler/src/library/cache-reset-fixtures/profile.json +++ b/packages/compiler/src/library/cache-reset-fixtures/profile.json @@ -2,7 +2,7 @@ "profile_format": 1, "name": "cache-reset", "entry": "missing.ts", - "emission": "c", + "emission": "llvm", "abi": { "prefix": "cr_", "init_symbol": "cr_init", diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index 15190bc80..08c9cbcc1 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -22,7 +22,7 @@ async function fixture(): Promise<{ options: EarlyLibraryCacheOptions; source: string; missing: string; - cPath: string; + llvmPath: string; irPath: string; sidecarPath: string; }> { @@ -30,13 +30,13 @@ async function fixture(): Promise<{ scratch.push(dir); const source = join(dir, "entry.ts"); const profilePath = join(dir, "profile.json"); - const cPath = join(dir, "entry.lib.ll"); + const llvmPath = join(dir, "entry.lib.ll"); const irPath = join(dir, "entry.lib.ir.json"); const sidecarPath = join(dir, "entry.lib.a.contract.json"); await Promise.all([ writeFile(source, "export function value(): number { return 1; }\n"), writeFile(profilePath, "{}\n"), - writeFile(cPath, "; generated llvm\n"), + writeFile(llvmPath, "; generated llvm\n"), writeFile(irPath, "{\"irVersion\":6}\n"), writeFile(sidecarPath, "{\"contract\":true}\n"), ]); @@ -44,7 +44,7 @@ async function fixture(): Promise<{ root: join(dir, "cache"), source, missing: join(dir, "missing.ts"), - cPath, + llvmPath, irPath, sidecarPath, options: { @@ -83,20 +83,20 @@ test("early library cache restores generated artifacts and metadata", async () = textDecoderLegacy: false, }; await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native, frontend: tracker.snapshot(), }); - await Promise.all([rm(f.cPath), rm(f.irPath), rm(f.sidecarPath)]); + await Promise.all([rm(f.llvmPath), rm(f.irPath), rm(f.sidecarPath)]); const alternateCPath = join(f.options.outDir, "entry.lib.c"); await writeFile(alternateCPath, "/* saved c backend */\n"); const hit = await readEarlyLibraryCache(f.root, f.options, null); expect(hit).not.toBeNull(); expect(hit?.native).toEqual(native); - expect(await readFile(f.cPath, "utf8")).toBe("; generated llvm\n"); + expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n"); expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); expect(await readFile(f.sidecarPath, "utf8")).toContain("contract"); expect(await readFile(alternateCPath, "utf8")).toBe("/* saved c backend */\n"); @@ -107,7 +107,7 @@ test("early library cache publishes after creating a fresh output directory", as const cacheRoot = await mkdtemp(join(tmpdir(), "scriptc-early-cache-root-")); scratch.push(cacheRoot); const outDir = join(f.options.outDir, "fresh-out"); - const cPath = join(outDir, "entry.lib.ll"); + const llvmPath = join(outDir, "entry.lib.ll"); const options = { ...f.options, outDir, emitIr: false }; const tracker = new FrontendInputTracker(); tracker.run(() => { @@ -118,9 +118,9 @@ test("early library cache publishes after creating a fresh output directory", as }); await mkdir(outDir); - await writeFile(cPath, "; generated llvm in fresh output\n"); + await writeFile(llvmPath, "; generated llvm in fresh output\n"); await publishEarlyLibraryCache(cacheRoot, options, { - cPath, + llvmPath, native: { backend: "llvm", regex: false, @@ -137,9 +137,9 @@ test("early library cache publishes after creating a fresh output directory", as frontend: tracker.snapshot(), }); - await rm(cPath); - expect((await readEarlyLibraryCache(cacheRoot, options, undefined))?.cPath).toBe(cPath); - expect(await readFile(cPath, "utf8")).toContain("fresh output"); + await rm(llvmPath); + expect((await readEarlyLibraryCache(cacheRoot, options, undefined))?.llvmPath).toBe(llvmPath); + expect(await readFile(llvmPath, "utf8")).toContain("fresh output"); }); test("early library cache hits refresh every payload's LRU time", async () => { @@ -147,7 +147,7 @@ test("early library cache hits refresh every payload's LRU time", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -191,7 +191,7 @@ test("early library cache misses on source edits and newly-resolved candidates", trackedFileExists(f.missing); }); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -212,14 +212,14 @@ test("early library cache misses on source edits and newly-resolved candidates", await writeFile(f.source, "export function value(): number { return 2; }\n"); expect(await readEarlyLibraryCache(f.root, f.options, null)).toBeNull(); - await writeFile(f.cPath, "; generated llvm v2\n"); + await writeFile(f.llvmPath, "; generated llvm v2\n"); const editedTracker = new FrontendInputTracker(); editedTracker.run(() => { trackedReadFile(f.source); trackedFileExists(f.missing); }); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -237,8 +237,8 @@ test("early library cache misses on source edits and newly-resolved candidates", }, frontend: editedTracker.snapshot(), }); - expect((await readEarlyLibraryCache(f.root, f.options, null))?.cPath).toBe(f.cPath); - expect(await readFile(f.cPath, "utf8")).toBe("; generated llvm v2\n"); + expect((await readEarlyLibraryCache(f.root, f.options, null))?.llvmPath).toBe(f.llvmPath); + expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm v2\n"); await writeFile(f.source, "export function value(): number { return 1; }\n"); await writeFile(f.missing, "export const appeared = true;\n"); @@ -265,7 +265,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit", const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -323,7 +323,7 @@ test("semantic library cache refuses token and directive edits", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -349,7 +349,7 @@ test("semantic library cache refuses token and directive edits", async () => { expect(await readSemanticLibraryCache(f.root, f.options, null)).toBeNull(); }); -test("semantic C cache accepts only line-preserving single-source edits", async () => { +test("semantic LLVM cache accepts comment edits that shift source lines", async () => { const f = await fixture(); const sourceBefore = await readFile(f.source, "utf8"); const semanticMod = { @@ -368,11 +368,11 @@ test("semantic C cache accepts only line-preserving single-source edits", async const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { - backend: "c", + backend: "llvm", regex: false, assert: false, inspect: false, @@ -391,10 +391,10 @@ test("semantic C cache accepts only line-preserving single-source edits", async await writeFile(f.source, `/* harmless */ ${sourceBefore}`); expect(await readSemanticLibraryCache(f.root, f.options, null)).not.toBeNull(); await writeFile(f.source, `// inserted line\n${sourceBefore}`); - expect(await readSemanticLibraryCache(f.root, f.options, null)).toBeNull(); + expect(await readSemanticLibraryCache(f.root, f.options, null)).not.toBeNull(); }); -test("semantic C cache refuses non-LF separator normalization", async () => { +test("semantic LLVM cache accepts line separator normalization", async () => { const f = await fixture(); for (const separator of ["\r", "\u2028", "\u2029"]) { const sourceBefore = [ @@ -405,11 +405,11 @@ test("semantic C cache refuses non-LF separator normalization", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { - backend: "c", + backend: "llvm", regex: false, assert: false, inspect: false, @@ -434,11 +434,11 @@ test("semantic C cache refuses non-LF separator normalization", async () => { }); await writeFile(f.source, sourceBefore.replace(separator, "\n")); - expect(await readSemanticLibraryCache(f.root, f.options, null)).toBeNull(); + expect(await readSemanticLibraryCache(f.root, f.options, null)).not.toBeNull(); } }); -test("semantic C cache refuses comment-only edits in multi-source graphs", async () => { +test("semantic LLVM cache rebases comment edits in multi-source graphs", async () => { const f = await fixture(); const imported = join(dirname(f.source), "helper.ts"); const entrySource = await readFile(f.source, "utf8"); @@ -463,11 +463,11 @@ test("semantic C cache refuses comment-only edits in multi-source graphs", async trackedReadFile(imported); }); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { - backend: "c", + backend: "llvm", regex: false, assert: false, inspect: false, @@ -487,10 +487,10 @@ test("semantic C cache refuses comment-only edits in multi-source graphs", async }); await writeFile(f.source, `// inserted entry comment\n${entrySource}`); - expect(await readSemanticLibraryCache(f.root, f.options, null)).toBeNull(); + expect(await readSemanticLibraryCache(f.root, f.options, null)).not.toBeNull(); await writeFile(f.source, entrySource); await writeFile(imported, `// inserted imported comment\n${importedSource}`); - expect(await readSemanticLibraryCache(f.root, f.options, null)).toBeNull(); + expect(await readSemanticLibraryCache(f.root, f.options, null)).not.toBeNull(); }); test("early library cache is separated by the host Node version", async () => { @@ -498,7 +498,7 @@ test("early library cache is separated by the host Node version", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -530,7 +530,7 @@ test("early library cache rejects corrupted artifacts and metadata", async () => const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); await publishEarlyLibraryCache(f.root, f.options, { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { @@ -563,7 +563,7 @@ test("disabled early library cache performs no reads or writes", async () => { const tracker = new FrontendInputTracker(); tracker.run(() => trackedReadFile(f.source)); const publish = { - cPath: f.cPath, + llvmPath: f.llvmPath, irPath: f.irPath, sidecarPath: f.sidecarPath, native: { diff --git a/packages/compiler/src/library/library-cache.ts b/packages/compiler/src/library/library-cache.ts index ef6ce5545..84d2d1a41 100644 --- a/packages/compiler/src/library/library-cache.ts +++ b/packages/compiler/src/library/library-cache.ts @@ -6,7 +6,7 @@ import { gzip, gunzip } from "node:zlib"; import type { IrModule } from "../ir/ir.js"; import { IR_VERSION } from "../ir/serialize.js"; import { frontendInputsSemanticallyMatch, frontendInputsStillMatch, validFrontendInputSnapshot, type FrontendInputSnapshot } from "../frontend/input-tracker.js"; -import { rebaseSourceLocations, semanticallyEqualSource, sourceLineRebaseIsIdentity } from "./semantic-source.js"; +import { rebaseSourceLocations, semanticallyEqualSource } from "./semantic-source.js"; import { compilerImplementationIdentity } from "./compiler-self-identity.js"; import { cacheKey as sharedCacheKey, @@ -40,7 +40,7 @@ interface EarlyLibraryCacheStamp { sources: CachedLibraryFile | null; }; native: { - backend: "c" | "llvm"; + backend: "llvm"; regex: boolean; assert: boolean; inspect: boolean; @@ -56,7 +56,7 @@ interface EarlyLibraryCacheStamp { } export interface EarlyLibraryNativeFeatures { - backend: "c" | "llvm"; + backend: "llvm"; regex: boolean; assert: boolean; inspect: boolean; @@ -88,7 +88,7 @@ export interface EarlyLibraryCacheOptions { } export interface EarlyLibraryCacheHit { - cPath: string; + llvmPath: string; irPath?: string; sidecarPath?: string; native: EarlyLibraryNativeFeatures; @@ -169,7 +169,7 @@ function stampIntegrity(stamp: Omit): strin return sharedStampIntegrity("early-library-stamp-v2", stamp); } -function outputPaths(options: EarlyLibraryCacheOptions, backend: "c" | "llvm") { +function outputPaths(options: EarlyLibraryCacheOptions, backend: "llvm") { return sharedOutputPaths(options, backend, ".lib"); } @@ -187,7 +187,7 @@ function archiveOutputPath(options: EarlyLibraryCacheOptions): string { function frontendOutputExclusions( options: EarlyLibraryCacheOptions, - backend: "c" | "llvm", + backend: "llvm", sidecarPath: string | undefined, ): ReturnType { return sharedFrontendOutputExclusions(options, backend, ".lib", [ @@ -258,7 +258,7 @@ export async function readEarlyLibraryCache( ) return null; const paths = outputPaths(options, stamp.native.backend); - await installBytes(translationUnit, paths.cPath); + await installBytes(translationUnit, paths.llvmPath); if (ir !== null) await installBytes(ir, paths.irPath); let sidecarPath: string | undefined; if (sidecar !== null) { @@ -273,7 +273,7 @@ export async function readEarlyLibraryCache( ...(stamp.files.sidecar === null ? [] : [join(directory, stamp.files.sidecar.name)]), ].map((cachePath) => utimes(cachePath, now, now).catch(() => undefined))); return { - cPath: paths.cPath, + llvmPath: paths.llvmPath, native: stamp.native, ...(ir !== null ? { irPath: paths.irPath } : {}), ...(sidecarPath !== undefined ? { sidecarPath } : {}), @@ -352,20 +352,6 @@ export async function readSemanticLibraryCache( ), ); if (semantic === null || semantic.changed.length === 0) return null; - // C source annotations are rendered through the entry source's line table, - // including imported offsets stamped with the entry path and synthetic - // byte-zero locations. Their line-only text cannot be rebased exactly for - // multi-source graphs or line-shifting edits. Keep TU reuse to the safe - // single-source, line-preserving subset; take the normal frontend path for - // the other uncommon trivia edits. - if (stamp.native.backend === "c") { - const entry = resolve(options.entryPath); - const change = semantic.changed.find((candidate) => candidate.path === entry); - if ( - previousSources.size > 1 || semantic.changed.length !== 1 || change === undefined || - !sourceLineRebaseIsIdentity(entry, change.previous, change.current) - ) return null; - } const mod = deserializeV8(irJson) as IrModule; if (mod.irVersion !== IR_VERSION) return null; rebaseSourceLocations(mod, previousSources, semantic.currentSources); @@ -410,7 +396,7 @@ export async function publishEarlyLibraryCache( return { name, digest: digest(await readFile(target)) }; }; const [translationUnit, ir, sidecar, semanticIr, sources] = await Promise.all([ - publishFile(result.cPath, "program.tu"), + publishFile(result.llvmPath, "program.tu"), result.irPath === undefined ? Promise.resolve(null) : publishFile(result.irPath, "program.ir.json"), result.sidecarPath === undefined ? Promise.resolve(null) : publishFile(result.sidecarPath, "contract.json"), result.semantic === undefined diff --git a/packages/compiler/src/library/library-profile.ts b/packages/compiler/src/library/library-profile.ts index 1a38b81e1..47b08209d 100644 --- a/packages/compiler/src/library/library-profile.ts +++ b/packages/compiler/src/library/library-profile.ts @@ -13,7 +13,7 @@ * "profile_format": 1, * "name": "", * "entry": "src/lib.ts", // ONE module, profile-relative - * "emission": "llvm" | "c", // pins the emission; no fallback + * "emission": "llvm", // pins the emission; no fallback * "optimization": "release" | "dev", // optional; default release * "abi": { * "prefix": "_", @@ -370,7 +370,7 @@ export interface LibraryProfile { /** Resolved absolute path of the ONE entry module. */ entry: string; /** The pinned emission — no fallback concept exists on the library path. */ - emission: "llvm" | "c"; + emission: "llvm"; /** Native optimizer posture. release preserves the production -O2 archive; * dev uses -O0 for fast iterative embedding builds. */ optimization: "release" | "dev"; @@ -511,8 +511,8 @@ export function loadLibraryProfile( const entryRel = req(p["entry"], "entry", "string"); if (entryRel === "") throw new ProfileError("'entry' must name the profile's one entry module"); const emission = req(p["emission"], "emission", "string"); - if (emission !== "llvm" && emission !== "c") { - throw new ProfileError(`'emission' must be "llvm" or "c", got '${emission}'`); + if (emission !== "llvm") { + throw new ProfileError(`'emission' must be "llvm"; C emission has been removed, got '${emission}'`); } const optimization = p["optimization"] === undefined ? "release" diff --git a/packages/compiler/src/library/semantic-source.ts b/packages/compiler/src/library/semantic-source.ts index 85c4fee2a..54bc9845b 100644 --- a/packages/compiler/src/library/semantic-source.ts +++ b/packages/compiler/src/library/semantic-source.ts @@ -175,7 +175,7 @@ function lineAt(starts: readonly number[], offset: number): number { } /** Build a cheap old-line to current-line mapper for semantically identical - * sources. C emission records only a location's line, not its byte offset, so + * sources. The emitted location metadata uses line numbers, so * representative semantic tokens anchor each old line across trivia edits. */ export function createSourceLineRebaser( path: string, @@ -220,7 +220,7 @@ export function sourceLineRebaseIsIdentity( } const oldStarts = lineStarts(previous); const newStarts = lineStarts(current); - // The C emitter counts only LF bytes. TypeScript also treats bare CR and + // The emitted location metadata counts LF bytes. TypeScript also treats bare CR and // the Unicode separators as line breaks, so normalizing one of those to LF // preserves semantic tokens while moving later annotations to a different // emitter line. Check every corresponding token, including multiple diff --git a/packages/compiler/src/native/arguments.test.ts b/packages/compiler/src/native/arguments.test.ts index d2592d1b9..e9aa2846e 100644 --- a/packages/compiler/src/native/arguments.test.ts +++ b/packages/compiler/src/native/arguments.test.ts @@ -3,13 +3,13 @@ import { parseNativeArguments } from "./arguments.js"; test("native build arguments preserve paths and select explicit output modes", () => { expect(parseNativeArguments([ - "build", "source with spaces.ts", "-o", "result with spaces", "--backend=c", "--emit=obj", - "--toolchain", "/installed/compiler.json", "--dev", "--strip", "--ffi=bindings.json", "--npm-static=one,@scope/two", + "build", "source with spaces.ts", "-o", "result with spaces", "--backend=llvm", "--emit=obj", + "--toolchain", "/installed/compiler.json", "--dev", "--strip", "--keep-llvm", "--ffi=bindings.json", "--npm-static=one,@scope/two", ], "default.json")).toEqual({ help: false, toolchainPath: "/installed/compiler.json", build: { - entryPath: "source with spaces.ts", outputPath: "result with spaces", backend: "c", outputKind: "obj", - optimization: "dev", strip: true, ffiProfilePath: "bindings.json", npmStatic: ["one", "@scope/two"], + entryPath: "source with spaces.ts", outputPath: "result with spaces", backend: "llvm", outputKind: "obj", + optimization: "dev", strip: true, keepLlvm: true, ffiProfilePath: "bindings.json", npmStatic: ["one", "@scope/two"], }, }); expect(parseNativeArguments(["--out=result", "--npm-static", "auto", "--", "-entry.ts"], "default.json")).toMatchObject({ @@ -26,7 +26,9 @@ test("native build rejects unknown, missing and ambiguous arguments", () => { [["main.ts", "-o", "--dev"], "requires a value"], [["main.ts", "-o=x", "extra.ts"], "unexpected argument"], [["main.ts", "-o=x", "--dynamic"], "unknown native compiler option"], - [["main.ts", "-o=x", "--backend=other"], "--backend must"], + [["main.ts", "-o=x", "--backend=other"], "LLVM is the only backend"], + [["main.ts", "-o=x", "--backend=c"], "LLVM is the only backend"], + [["main.ts", "-o=x", "--emit=c"], "--emit must"], [["main.ts", "-o=x", "--emit=other"], "--emit must"], ] as const) expect(() => parseNativeArguments(args, "toolchain.json")).toThrow(message); }); diff --git a/packages/compiler/src/native/arguments.ts b/packages/compiler/src/native/arguments.ts index 828f0b278..d2455e6a0 100644 --- a/packages/compiler/src/native/arguments.ts +++ b/packages/compiler/src/native/arguments.ts @@ -10,10 +10,11 @@ export function parseNativeArguments(args: readonly string[], defaultToolchain: let toolchainPath = defaultToolchain; let entryPath = ""; let outputPath = ""; - let backend: "c" | "llvm" = "llvm"; - let outputKind: "exe" | "obj" | "c" | "llvm" = "exe"; + let backend = "llvm" as const; + let outputKind: "exe" | "obj" | "llvm" = "exe"; let optimization: "release" | "dev" = "release"; let strip = false; + let keepLlvm = false; let help = false; let ffiProfilePath: string | undefined; let npmStatic: string[] | "auto" | undefined; @@ -24,6 +25,7 @@ export function parseNativeArguments(args: readonly string[], defaultToolchain: if (!positional && arg === "--") { positional = true; continue; } if (!positional && arg === "--dev") { optimization = "dev"; continue; } if (!positional && arg === "--strip") { strip = true; continue; } + if (!positional && arg === "--keep-llvm") { keepLlvm = true; continue; } if (!positional && arg.startsWith("-")) { const equals = arg.indexOf("="); const name = equals < 0 ? arg : arg.slice(0, equals); @@ -37,10 +39,10 @@ export function parseNativeArguments(args: readonly string[], defaultToolchain: else if (name === "--ffi") ffiProfilePath = value; else if (name === "--npm-static") npmStatic = value === "auto" ? "auto" : value.split(","); else if (name === "--backend") { - if (value !== "c" && value !== "llvm") throw new Error("--backend must be c or llvm"); + if (value !== "llvm") throw new Error("LLVM is the only backend"); backend = value; } else if (name === "--emit") { - if (value !== "exe" && value !== "obj" && value !== "c" && value !== "llvm") throw new Error("--emit must be exe, obj, c, or llvm"); + if (value !== "exe" && value !== "obj" && value !== "llvm") throw new Error("--emit must be exe, obj, or llvm"); outputKind = value; } continue; @@ -55,6 +57,7 @@ export function parseNativeArguments(args: readonly string[], defaultToolchain: help, toolchainPath, build: { entryPath, outputPath, backend, outputKind, optimization, strip, + ...(keepLlvm ? { keepLlvm: true } : {}), ...(ffiProfilePath === undefined ? {} : { ffiProfilePath }), ...(npmStatic === undefined ? {} : { npmStatic }), }, @@ -63,10 +66,11 @@ export function parseNativeArguments(args: readonly string[], defaultToolchain: export const NATIVE_HELP = `Usage: scriptc-native build -o [options] - --backend Code generation backend (default: llvm) - --emit Output artifact (default: exe) + --backend Code generation backend (default: llvm) + --emit Output artifact (default: exe) --dev Disable optimization and include debug information --strip Strip executable symbols + --keep-llvm Keep the generated LLVM at .ll --ffi Native FFI bindings and link inputs --npm-static Compile comma-separated packages, or auto --toolchain Native toolchain manifest (default: beside executable) diff --git a/packages/compiler/src/native/driver.test.ts b/packages/compiler/src/native/driver.test.ts new file mode 100644 index 000000000..b2cf39b9f --- /dev/null +++ b/packages/compiler/src/native/driver.test.ts @@ -0,0 +1,72 @@ +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterEach, expect, test, vi } from "vitest"; +import { buildNative, type NativeToolchain } from "./driver.js"; +import { runNativeFrontend } from "../frontend/pipeline-native.js"; +import { emitNativeObject } from "../backend/native-tools.js"; +import { MACOS_ARM64_TARGET } from "../backend/targets.js"; +import { VOID, type IrModule } from "../ir/ir.js"; + +vi.mock("../frontend/pipeline-native.js", () => ({ runNativeFrontend: vi.fn() })); +vi.mock("../backend/native-tools.js", async (original) => ({ + ...await original(), emitNativeObject: vi.fn(), +})); + +const directories: string[] = []; +afterEach(() => { + vi.resetAllMocks(); + for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +test.each([false, true])("object generation releases frontend resources and preserves outputs on failure (%s)", (fail) => { + const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-driver-")); + directories.push(directory); + const entry = join(directory, "entry.ts"); + const output = join(directory, "output.o"); + const llvmPath = output + ".ll"; + writeFileSync(output, "previous object"); + writeFileSync(llvmPath, "previous LLVM"); + const module: IrModule = { + irVersion: 13, sourceFile: entry, entry: "%main", + functions: [{ name: "%main", params: [], locals: [], body: [], returnType: VOID, loc: { file: entry, start: 0, end: 0 } }], + }; + const dispose = vi.fn(); + vi.mocked(runNativeFrontend).mockReturnValue({ + preflight: [], entryText: () => "", entryExports: () => new Map(), + entryContract: () => { throw new Error("unexpected contract request"); }, + sourceTexts: () => new Map(), npmStatic: [], npmImportSites: new Map(), dispose, + lower: () => ({ module, diagnostics: [], runtimeFences: [], stats: { + statementsTotal: 0, statementsFailed: 0, statementsIsland: 0, functionsSkipped: 0, + } }), + }); + let emitted = ""; + vi.mocked(emitNativeObject).mockImplementation((options) => { + expect(dispose).toHaveBeenCalledTimes(1); + emitted = readFileSync(options.inputPath, "utf8"); + expect(emitted).toContain("define i32 @main"); + if (fail) throw new Error("codegen failed"); + writeFileSync(options.outputPath, "new object"); + }); + const target = MACOS_ARM64_TARGET; + const toolchain: NativeToolchain = { + compilerVersion: "0.0.0", target, helper: target.helper, ts7Executable: "unused", + helperExecutable: "unused", helperPackageRoot: "unused", runtimePackRoot: "unused", + linker: "unused", linkerArgs: [], dsymutil: "unused", + }; + const build = () => buildNative({ + entryPath: entry, outputPath: output, backend: "llvm", outputKind: "obj", + optimization: "release", strip: true, keepLlvm: true, + }, toolchain); + if (fail) { + expect(build).toThrow("codegen failed"); + expect(readFileSync(output, "utf8")).toBe("previous object"); + expect(readFileSync(llvmPath, "utf8")).toBe("previous LLVM"); + } else { + expect(build()).toMatchObject({ outputPath: output, llvmPath }); + expect(readFileSync(output, "utf8")).toBe("new object"); + expect(readFileSync(llvmPath, "utf8")).toBe(emitted); + } + expect(dispose).toHaveBeenCalledTimes(1); + expect(readdirSync(directory).sort()).toEqual(["output.o", "output.o.ll"]); +}); diff --git a/packages/compiler/src/native/driver.ts b/packages/compiler/src/native/driver.ts index d09dffff2..31094f232 100644 --- a/packages/compiler/src/native/driver.ts +++ b/packages/compiler/src/native/driver.ts @@ -1,12 +1,12 @@ /** Static executable compiler. The TS7 parser/checker and native toolchain - * are external native processes; lowering, validation and both emitters run + * are external native processes; lowering, validation and LLVM emission run * inside this binary. Installed paths are supplied by the distribution. */ -import { appendFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; -import { emitCModuleChunks } from "../backend/c/c-emitter.js"; import { emitLlvmModule } from "../backend/llvm/emitter.js"; import { executableLinkFeatures } from "../backend/executable-features.js"; import { executableLinkInputs } from "../backend/link-plan-core.js"; +import type { NativeLinkFeatures } from "../backend/native-link-info.js"; import { stageNativeRuntimePack } from "../backend/runtime-pack-native.js"; import { emitNativeObject, requireNativeArtifact, runNativeTool } from "../backend/native-tools.js"; import type { NativeHelperSpec, NativeTargetSpec } from "../backend/targets.js"; @@ -23,9 +23,6 @@ export interface NativeToolchain { helperExecutable: string; helperPackageRoot: string; runtimePackRoot: string; - runtimeIncludeDirectory: string; - cCompiler: string; - cCompilerArgs: string[]; linker: string; linkerArgs: string[]; dsymutil: string; @@ -34,29 +31,27 @@ export interface NativeToolchain { export interface NativeBuildOptions { entryPath: string; outputPath: string; - backend: "c" | "llvm"; - outputKind: "exe" | "obj" | "c" | "llvm"; + backend: "llvm"; + outputKind: "exe" | "obj" | "llvm"; optimization: "release" | "dev"; strip: boolean; + keepLlvm?: boolean; ffiProfilePath?: string; npmStatic?: readonly string[] | "auto"; } export interface NativeBuildResult { outputPath: string; + llvmPath?: string; stats: LowerStats; } -export function buildNative(options: NativeBuildOptions, toolchain: NativeToolchain): NativeBuildResult { - const entry = resolve(options.entryPath); - const output = resolve(options.outputPath); - if (entry === output) throw new Error("output path must differ from the entry source"); - let ffi: FfiProfile | null = null; - if (options.ffiProfilePath !== undefined) { - const loaded = loadFfiProfile(options.ffiProfilePath); - if (!loaded.ok) throw new Error(JSON.stringify(loaded.diagnostics)); - ffi = loaded.profile; - } +/** Keep the program graph and its TS7 session inside the frontend phase. + * Native object generation can need several GB of its own working memory. */ +function emitNativeInput( + options: NativeBuildOptions, toolchain: NativeToolchain, + entry: string, ffi: FfiProfile | null, path: string, +): { features: NativeLinkFeatures; stats: LowerStats } { const frontend = runNativeFrontend(entry, toolchain.ts7Executable, options.npmStatic); try { if (frontend.preflight.length !== 0) throw new Error(JSON.stringify(frontend.preflight)); @@ -68,81 +63,80 @@ export function buildNative(options: NativeBuildOptions, toolchain: NativeToolch const errors = validateModule(module); if (errors.length !== 0) throw new Error(JSON.stringify(errors)); const features = executableLinkFeatures(module, false); - const sourceOutput = options.outputKind === "c" || options.outputKind === "llvm"; - const backend = sourceOutput ? options.outputKind : options.backend; const debug = options.optimization === "dev" && !options.strip; const debugSources = debug ? frontend.sourceTexts() : new Map(); - const chunks = backend === "c" - ? emitCModuleChunks(module, undefined, { debugSources }) - : [emitLlvmModule(module, { - targetTriple: toolchain.target.llvmTriple, - pointerBits: toolchain.target.pointerBits, - wasi: toolchain.target.platform === "wasi", - runtimeAbiMarker: true, - ...(debug ? { debugSources } : {}), - })]; - const writeSource = (path: string): void => { - writeFileSync(path, ""); - for (const chunk of chunks) appendFileSync(path, chunk); - }; - mkdirSync(dirname(output), { recursive: true }); - // A sibling temporary directory keeps installation on the same volume - // and preserves the basename used by Mach-O's ad-hoc signature. - const stage = mkdtempSync(join(dirname(output), ".scriptc-native-")); - try { - const outputDirectory = join(stage, "output"); - const inputDirectory = join(stage, "input"); - mkdirSync(outputDirectory); - mkdirSync(inputDirectory); - const stagedOutput = join(outputDirectory, basename(output)); - if (sourceOutput) { - writeSource(stagedOutput); - } else { - const input = join(inputDirectory, backend === "c" ? "program.c" : "program.ll"); - const object = options.outputKind === "obj" ? stagedOutput : join(inputDirectory, "program" + toolchain.target.outputSuffixes.obj); - writeSource(input); - if (backend === "llvm") { - emitNativeObject({ - executable: toolchain.helperExecutable, packageRoot: toolchain.helperPackageRoot, - compilerVersion: toolchain.compilerVersion, target: toolchain.target, helper: toolchain.helper, - inputPath: input, outputPath: object, sourcePath: entry, optimization: options.optimization, - }); - } else { - runNativeTool(toolchain.cCompiler, [ - ...toolchain.cCompilerArgs, "-std=c11", "-target", toolchain.target.linkerTargetTriple, - "-fno-strict-aliasing", options.optimization === "dev" ? "-O0" : "-O2", - ...(debug ? ["-gline-tables-only", "-gno-column-info"] : []), - "-I", toolchain.runtimeIncludeDirectory, "-c", input, "-o", object, - ]); - requireNativeArtifact(object); - } - if (options.outputKind === "exe") { - const runtime = stageNativeRuntimePack(toolchain.runtimePackRoot, join(stage, "runtime"), - toolchain.target, toolchain.compilerVersion, features, options.optimization); - const plan = executableLinkInputs({ - target: toolchain.target, programObject: object, - ffiLibraries: ffi?.libraries ?? [], ffiSystemLibraries: ffi?.systemLibraries ?? [], - ffiFrameworks: ffi?.frameworks ?? [], - runtimeObjects: runtime.runtimeObjects, runtimeArchives: runtime.archives, - runtimeSystemLibraries: runtime.systemLibraries, optimization: options.optimization, strip: options.strip, - }); - runNativeTool(toolchain.linker, [ - ...toolchain.linkerArgs, ...plan.driverFlags, ...plan.inputs, - ...plan.systemLibraries.map((name) => `-l${name}`), "-o", stagedOutput, - ]); - requireNativeArtifact(stagedOutput); - if (toolchain.target.platform === "darwin" && options.optimization === "dev" && !options.strip) { - runNativeTool(toolchain.dsymutil, [stagedOutput, "-o", stagedOutput + ".dSYM"]); - rmSync(output + ".dSYM", { recursive: true, force: true }); - renameSync(stagedOutput + ".dSYM", output + ".dSYM"); - } + const llvm = emitLlvmModule(module, { + targetTriple: toolchain.target.llvmTriple, + pointerBits: toolchain.target.pointerBits, + wasi: toolchain.target.platform === "wasi", + runtimeAbiMarker: true, + ...(debug ? { debugSources } : {}), + }); + writeFileSync(path, llvm); + return { features, stats: lowered.stats }; + } finally { frontend.dispose(); } +} + +export function buildNative(options: NativeBuildOptions, toolchain: NativeToolchain): NativeBuildResult { + const entry = resolve(options.entryPath); + const output = resolve(options.outputPath); + if (entry === output) throw new Error("output path must differ from the entry source"); + let ffi: FfiProfile | null = null; + if (options.ffiProfilePath !== undefined) { + const loaded = loadFfiProfile(options.ffiProfilePath); + if (!loaded.ok) throw new Error(JSON.stringify(loaded.diagnostics)); + ffi = loaded.profile; + } + mkdirSync(dirname(output), { recursive: true }); + // A sibling temporary directory keeps installation on the same volume + // and preserves the basename used by Mach-O's ad-hoc signature. + const stage = mkdtempSync(join(dirname(output), ".scriptc-native-")); + try { + const outputDirectory = join(stage, "output"); + const inputDirectory = join(stage, "input"); + mkdirSync(outputDirectory); + mkdirSync(inputDirectory); + const stagedOutput = join(outputDirectory, basename(output)); + const sourceOutput = options.outputKind === "llvm"; + const input = sourceOutput ? stagedOutput : join(inputDirectory, "program.ll"); + const prepared = emitNativeInput(options, toolchain, entry, ffi, input); + if (!sourceOutput) { + const object = options.outputKind === "obj" ? stagedOutput : join(inputDirectory, "program" + toolchain.target.outputSuffixes.obj); + emitNativeObject({ + executable: toolchain.helperExecutable, packageRoot: toolchain.helperPackageRoot, + compilerVersion: toolchain.compilerVersion, target: toolchain.target, helper: toolchain.helper, + inputPath: input, outputPath: object, sourcePath: entry, optimization: options.optimization, + }); + if (options.outputKind === "exe") { + const runtime = stageNativeRuntimePack(toolchain.runtimePackRoot, join(stage, "runtime"), + toolchain.target, toolchain.compilerVersion, prepared.features, options.optimization); + const plan = executableLinkInputs({ + target: toolchain.target, programObject: object, + ffiLibraries: ffi?.libraries ?? [], ffiSystemLibraries: ffi?.systemLibraries ?? [], + ffiFrameworks: ffi?.frameworks ?? [], + runtimeObjects: runtime.runtimeObjects, runtimeArchives: runtime.archives, + runtimeSystemLibraries: runtime.systemLibraries, optimization: options.optimization, strip: options.strip, + }); + runNativeTool(toolchain.linker, [ + ...toolchain.linkerArgs, ...plan.driverFlags, ...plan.inputs, + ...plan.systemLibraries.map((name) => `-l${name}`), "-o", stagedOutput, + ]); + requireNativeArtifact(stagedOutput); + if (toolchain.target.platform === "darwin" && options.optimization === "dev" && !options.strip) { + runNativeTool(toolchain.dsymutil, [stagedOutput, "-o", stagedOutput + ".dSYM"]); + rmSync(output + ".dSYM", { recursive: true, force: true }); + renameSync(stagedOutput + ".dSYM", output + ".dSYM"); } } - renameSync(stagedOutput, output); - if (options.outputKind === "exe" && toolchain.target.platform === "darwin" && (options.optimization !== "dev" || options.strip)) { - rmSync(output + ".dSYM", { recursive: true, force: true }); - } - } finally { rmSync(stage, { recursive: true, force: true }); } - return { outputPath: output, stats: lowered.stats }; - } finally { frontend.dispose(); } + } + if (options.keepLlvm && !sourceOutput) renameSync(join(inputDirectory, "program.ll"), output + ".ll"); + renameSync(stagedOutput, output); + if (options.outputKind === "exe" && toolchain.target.platform === "darwin" && (options.optimization !== "dev" || options.strip)) { + rmSync(output + ".dSYM", { recursive: true, force: true }); + } + return { + outputPath: output, stats: prepared.stats, + ...(options.keepLlvm && !sourceOutput ? { llvmPath: output + ".ll" } : {}), + }; + } finally { rmSync(stage, { recursive: true, force: true }); } } diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index 4c3fb4f46..7197c61d8 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -9,8 +9,8 @@ test("native toolchain installation paths relocate with the manifest", () => { const path = join(root, "toolchain.json"); const manifest: NativeToolchainManifest = { schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "macos-arm64", - ts7: "typescript/tsc", llvm_package: "llvm", runtime_pack: "runtime", runtime_headers: "headers", - c_compiler: "clang", c_compiler_args: [], linker: "tools/linker", linker_args: ["cc"], dsymutil: "dsymutil", + ts7: "typescript/tsc", llvm_package: "llvm", runtime_pack: "runtime", + linker: "tools/linker", linker_args: ["cc"], dsymutil: "dsymutil", }; try { writeFileSync(path, JSON.stringify(manifest)); @@ -19,7 +19,6 @@ test("native toolchain installation paths relocate with the manifest", () => { expect(toolchain.helperExecutable).toBe(join(root, "llvm/bin/scriptc-llvm-codegen")); expect(toolchain.runtimePackRoot).toBe(join(root, "runtime")); expect(toolchain.linker).toBe(join(root, "tools/linker")); - expect(toolchain.cCompiler).toBe("clang"); expect(toolchain.linkerArgs).toEqual(["cc"]); expect(toolchain.target.name).toBe("macos-arm64"); for (const invalid of [null, [], {}, { ...manifest, schema: "other" }, { ...manifest, target: "other" }, diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index 0452bf78d..c5adc8270 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -12,9 +12,6 @@ export interface NativeToolchainManifest { ts7: string; llvm_package: string; runtime_pack: string; - runtime_headers: string; - c_compiler: string; - c_compiler_args: string[]; linker: string; linker_args: string[]; dsymutil: string; @@ -33,10 +30,10 @@ export function loadNativeToolchain(path: string): NativeToolchain { if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid native toolchain manifest"); const value = raw as Record; if (value.schema !== "scriptc.native-toolchain.v1") throw new Error("unsupported native toolchain manifest schema"); - for (const name of ["compiler_version", "target", "ts7", "llvm_package", "runtime_pack", "runtime_headers", "c_compiler", "linker", "dsymutil"]) { + for (const name of ["compiler_version", "target", "ts7", "llvm_package", "runtime_pack", "linker", "dsymutil"]) { if (typeof value[name] !== "string" || value[name] === "") throw new Error(`native toolchain requires ${name}`); } - for (const name of ["c_compiler_args", "linker_args"]) { + for (const name of ["linker_args"]) { if (!Array.isArray(value[name]) || !(value[name] as unknown[]).every((arg) => typeof arg === "string")) { throw new Error(`native toolchain requires a string array for ${name}`); } @@ -53,8 +50,6 @@ export function loadNativeToolchain(path: string): NativeToolchain { target, helper: target.helper, helperPackageRoot, helperExecutable: join(helperPackageRoot, "bin", target.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen"), runtimePackRoot: pathFrom(root, manifest.runtime_pack), - runtimeIncludeDirectory: pathFrom(root, manifest.runtime_headers), - cCompiler: commandFrom(root, manifest.c_compiler), cCompilerArgs: manifest.c_compiler_args, linker: commandFrom(root, manifest.linker), linkerArgs: manifest.linker_args, dsymutil: commandFrom(root, manifest.dsymutil), }; diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts index 8ec924cd7..850007673 100644 --- a/packages/compiler/src/startup-cache.ts +++ b/packages/compiler/src/startup-cache.ts @@ -6,7 +6,6 @@ export type { EarlyExecutableRouteOptions } from "./executable/executable-cache. export { executableNativeEnvironmentFingerprint, configuredTargetPlatform, - legacyCExecutablePathRequested, resolveCc, targetPlatform, } from "./backend/external-c.js"; @@ -15,7 +14,6 @@ export { executableLinkerEnvironmentFingerprint, resolvePlatformLinker, } from "./backend/linker.js"; -import { legacyCExecutablePathRequested } from "./backend/external-c.js"; import { nativeCodegenTarget, type NativeTargetSpec } from "./backend/targets.js"; /** Whether an ordinary LLVM executable can take the helper/object plus @@ -25,8 +23,6 @@ import { nativeCodegenTarget, type NativeTargetSpec } from "./backend/targets.js export function precompiledRuntimePackTarget( env: NodeJS.ProcessEnv = process.env, ): NativeTargetSpec | null { - if (env["SCRIPTC_RUNTIME_PACK"] === "0" || - env["SCRIPTC_FETCH_CURL"] === "1" || - legacyCExecutablePathRequested(env)) return null; + if (env["SCRIPTC_FETCH_CURL"] === "1") return null; return nativeCodegenTarget(env); } diff --git a/packages/compiler/test/buffer-from-dynamic.test.ts b/packages/compiler/test/buffer-from-dynamic.test.ts index 4438170b3..268a80dea 100644 --- a/packages/compiler/test/buffer-from-dynamic.test.ts +++ b/packages/compiler/test/buffer-from-dynamic.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { compile } from "../src/index.js"; -test.each(["c", "llvm"] as const)("untyped Buffer inputs keep explicit object-boundary refusals (%s)", async (backend) => { +test.each(["llvm"] as const)("untyped Buffer inputs keep explicit object-boundary refusals (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-buffer-from-")); try { const entry = join(dir, "main.cjs"); diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 2297541fe..49b328cbc 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -1,5 +1,4 @@ import { expect, test } from "vitest"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { BOOL, F64, VOID, bytesOf, type IrBytesElem, type IrExpr, type IrLocal, type IrModule, type IrStmt } from "../src/ir/ir.js"; import { validateModule } from "../src/ir/validate.js"; @@ -242,20 +241,6 @@ function integerLoopFixture(mutatesIndex = false): IrModule { }; } -test("C emission specializes typed-array element access by static element kind", () => { - const mod = fixture(); - expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); - for (const elem of elems) { - expect(c).toContain(`sc_bytes_get_${elem}(`); - expect(c).toContain(`sc_bytes_set_${elem}(`); - } - // The only generic calls are the shared cold invalid-index trap funnel. - expect(c.match(/\bscr_bytes_get\(/g)).toHaveLength(2); - expect(c).not.toMatch(/\bscr_bytes_set\(/); - expect(c).not.toContain("scr_bytes_retain("); -}); - test("LLVM emission performs typed-array element access directly on the valid path", () => { const mod = fixture(); expect(validateModule(mod)).toEqual([]); @@ -274,21 +259,20 @@ test("LLVM emission performs typed-array element access directly on the valid pa test("side-effecting indices retain the receiver snapshot", () => { const mod = sideEffectFixture(); expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); + const ll = emitLlvmModule(mod); - expect(c).toContain("scr_bytes_retain(sc_l_b_0)"); + expect(ll).toContain("call ptr @scr_bytes_retain_v"); - expect(c.match(/\bscr_bytes_get\(/g)).toHaveLength(2); + expect(ll).toContain("call double @scr_bytes_get"); }); test("receiver assignments nested in numeric operands retain the receiver snapshot", () => { const mod = receiverReassignmentFixture(); expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); + const ll = emitLlvmModule(mod); - expect(c).toContain("scr_bytes_retain(sc_l_read)"); - expect(c).toContain("scr_bytes_retain(sc_l_write)"); + // Two receiver snapshots plus two retains per yielded bytes assignment // (one for the RHS temp and one for the binding's stored reference). expect(ll.match(/call ptr @scr_bytes_retain_v/g)).toHaveLength(6); @@ -297,13 +281,8 @@ test("receiver assignments nested in numeric operands retain the receiver snapsh test("canonical byte loops keep their induction variable and indices integral", () => { const mod = integerLoopFixture(); expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); - const ll = emitLlvmModule(mod); - expect(c).toContain("uint64_t sc_i"); - expect(c).toContain("sc_bytes_get_u8_u64("); - expect(c).toContain("sc_bytes_set_u8_u64("); - expect(c).not.toContain("sc_bytes_index_checked("); + const ll = emitLlvmModule(mod); expect(ll).toContain("alloca i64 ; integer induction index"); expect(ll).toContain("icmp ult i64"); @@ -313,11 +292,9 @@ test("canonical byte loops keep their induction variable and indices integral", test("a body mutation keeps the byte loop on the general f64 path", () => { const mod = integerLoopFixture(true); expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); + const ll = emitLlvmModule(mod); - expect(c).not.toContain("integer induction index"); - expect(c).not.toContain("_u64("); expect(ll).not.toContain("integer induction index"); expect(ll).toContain("bytes.index.range"); expect(ll).toContain("fptoui double"); diff --git a/packages/compiler/test/collection-keys.test.ts b/packages/compiler/test/collection-keys.test.ts index 351045030..0d193df51 100644 --- a/packages/compiler/test/collection-keys.test.ts +++ b/packages/compiler/test/collection-keys.test.ts @@ -6,8 +6,7 @@ import { analyze } from "../src/index.js"; import { BOOL, DYN, F64, STRING, UNDEFINED_T, VOID, arrayOf, isIdentityCollectionKey, isSupportedMapKey, isSupportedSetElem, mapOf, setOf, type IrExpr, type IrModule, type IrType } from "../src/ir/ir.js"; import { validateModule } from "../src/ir/validate.js"; import { IR_VERSION } from "../src/ir/serialize.js"; -import { mapKeyAccess, mapKeyKindC } from "../src/backend/c/types.js"; -import { mapKeyAccess as llvmAccess, mapKeyKindNum } from "../src/backend/llvm/shapes.js"; +import { mapKeyAccess, mapKeyKindNum } from "../src/backend/llvm/shapes.js"; const reference: IrType = { kind: "record", shapeId: "key" }; const references: IrType[] = [reference, { kind: "object", className: "Key" }, arrayOf(F64), { kind: "symbol" }, { kind: "netServer" }]; @@ -41,13 +40,11 @@ test("checked collection slots refuse snapshot and adapting reference conversion } }); -test.each(references)("identity key %j uses the reference ABI in both emitters", (type) => { +test.each(references)("identity key %j uses the reference ABI in LLVM", (type) => { expect(isIdentityCollectionKey(type)).toBe(true); expect(isSupportedMapKey(type)).toBe(true); expect(isSupportedSetElem(type)).toBe(true); expect(mapKeyAccess(type)).toBe("ref"); - expect(llvmAccess(type)).toBe("ref"); - expect(mapKeyKindC(type)).toBe("SCR_MAP_KEY_REF"); expect(mapKeyKindNum(type)).toBe(2); }); @@ -55,8 +52,6 @@ test("boxed reference keys select payload identity, not wrapper identity", () => expect(isSupportedMapKey(union, references)).toBe(true); expect(isSupportedSetElem(union, references)).toBe(true); expect(mapKeyAccess(union)).toBe("ref"); - expect(llvmAccess(union)).toBe("ref"); - expect(mapKeyKindC(union)).toBe("SCR_MAP_KEY_UNION_REF"); expect(mapKeyKindNum(union)).toBe(3); }); @@ -73,16 +68,16 @@ test("unresolved, empty and mixed-value unions cannot enter the identity ABI", ( }); test("primitive key ABI constants remain stable", () => { - expect([mapKeyKindC(F64), mapKeyKindNum(F64), mapKeyAccess(F64)]).toEqual(["SCR_MAP_KEY_F64", 0, "f64"]); - expect([mapKeyKindC(STRING), mapKeyKindNum(STRING), mapKeyAccess(STRING)]).toEqual(["SCR_MAP_KEY_STR", 1, "str"]); + expect([mapKeyKindNum(F64), mapKeyAccess(F64)]).toEqual([0, "f64"]); + expect([mapKeyKindNum(STRING), mapKeyAccess(STRING)]).toEqual([1, "str"]); }); test("unknown keys select value equality separately from reference identity", () => { expect(isIdentityCollectionKey(DYN)).toBe(false); expect(isSupportedMapKey(DYN)).toBe(true); expect(isSupportedSetElem(DYN)).toBe(true); - expect([mapKeyKindC(DYN), mapKeyKindNum(DYN), mapKeyAccess(DYN), llvmAccess(DYN)]) - .toEqual(["SCR_MAP_KEY_DYN", 4, "ref", "ref"]); + expect([mapKeyKindNum(DYN), mapKeyAccess(DYN)]) + .toEqual([4, "ref"]); }); test.each(["map", "set"] as const)("validator checks the arms behind a %s key union", (kind) => { diff --git a/packages/compiler/test/constant-table-emission.test.ts b/packages/compiler/test/constant-table-emission.test.ts index 5c43581dd..9b648766e 100644 --- a/packages/compiler/test/constant-table-emission.test.ts +++ b/packages/compiler/test/constant-table-emission.test.ts @@ -3,11 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { findConstantNumericTables } from "../src/ir/constant-tables.js"; -test("both backends use guarded native constants and retain array initialization", async () => { +test("LLVM uses guarded native constants and retain array initialization", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-constant-tables-")); try { const entry = join(dir, "main.ts"); @@ -23,14 +22,9 @@ console.log(lookup(1), mutated[0] * 1); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const mod = deserializeModule(await readFile(outPath, "utf8")); expect([...findConstantNumericTables(mod).values()].map((t) => t.values)).toEqual([[3, -0, Infinity]]); - const c = emitCModule(mod); + const llvm = emitLlvmModule(mod); - expect(c).toContain("static const double sc_const_numbers_0[] = { 3.0, -0.0, INFINITY }"); - expect(c).toContain("if (a != NULL && i >= 0.0 && i < 3.0)"); - expect(c).toContain("if ((double)index == i)"); - expect(c).toMatch(/= sc_const_numbers_0_get\(/); - expect(c).toContain("return scr_arr_get_number(a, i)"); - expect(c).toContain("scr_arr_push_f64("); + expect(llvm).toContain("@sc_const_numbers_0 = private constant [3 x double]"); expect(llvm).toContain("%initialized = icmp ne ptr %a, null"); expect(llvm).toContain("br i1 %safe, label %convert, label %fallback"); @@ -45,18 +39,17 @@ console.log(lookup(1), mutated[0] * 1); }); test.each([ - ["2970-constant-numeric-tables.ts", "2.0, -0.0, 1.5, INFINITY, -INFINITY, 8.0, NAN"], - ["2971-constant-table-modules/main.ts", "2.0, 5.0, 11.0"], -])("%s exercises specialization and rejects its mutated/escaping tables", async (fixture, expected) => { + "2970-constant-numeric-tables.ts", + "2971-constant-table-modules/main.ts", +])("%s specializes only immutable nonescaping tables", async (fixture) => { const dir = await mkdtemp(join(tmpdir(), "scriptc-constant-tables-")); try { - const outPath = join(dir, "main.c"); + const outPath = join(dir, "main.ll"); const entry = join(import.meta.dirname, "../../../tests/corpus", fixture); - const result = await compile(entry, { outDir: dir, outPath, outputKind: "c" }); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - const c = await readFile(outPath, "utf8"); - expect(c.match(/static const double sc_const_numbers_/g)).toHaveLength(1); - expect(c).toContain(`static const double sc_const_numbers_0[] = { ${expected} };`); - expect(c).toMatch(/= sc_const_numbers_0_get\(/); + const llvm = await readFile(outPath, "utf8"); + expect(llvm).toContain("@sc_const_numbers_0 = private constant"); + expect(llvm).toMatch(/call double @sc_const_numbers_0_get\(/); } finally { await rm(dir, { recursive: true, force: true }); } }); diff --git a/packages/compiler/test/discriminated-json.test.ts b/packages/compiler/test/discriminated-json.test.ts index 5afe7ed4d..f040d612f 100644 --- a/packages/compiler/test/discriminated-json.test.ts +++ b/packages/compiler/test/discriminated-json.test.ts @@ -7,8 +7,8 @@ import { compile } from "../src/index.js"; // A checked assertion intentionally validates more than Node's erased // assertion. Test rejection here; valid inputs have differential corpus -// coverage on both backends. -for (const backend of ["c", "llvm"] as const) { +// coverage through LLVM. +for (const backend of ["llvm"] as const) { test(`checked discriminator rejection (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-discriminator-")); try { diff --git a/packages/compiler/test/emit-c.test.ts b/packages/compiler/test/emit-llvm.test.ts similarity index 97% rename from packages/compiler/test/emit-c.test.ts rename to packages/compiler/test/emit-llvm.test.ts index 7b15ac3ba..80a197c5b 100644 --- a/packages/compiler/test/emit-c.test.ts +++ b/packages/compiler/test/emit-llvm.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { expect, test } from "vitest"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { compileC } from "../src/backend/native-toolchain.js"; import { validateModule } from "../src/ir/validate.js"; import { fibModule } from "./fixtures/fib-ir.js"; @@ -15,8 +15,8 @@ const execFileAsync = promisify(execFile); async function emitCompileRun(mod: IrModule, sanitize = true): Promise { expect(validateModule(mod)).toEqual([]); const dir = await mkdtemp(join(tmpdir(), "scriptc-emit-")); - const cPath = join(dir, "program.c"); - await writeFile(cPath, emitCModule(mod)); + const cPath = join(dir, "program.ll"); + await writeFile(cPath, emitLlvmModule(mod)); const outPath = join(dir, "program"); await compileC({ cPath, outPath, sanitize }); const { stdout } = await execFileAsync(outPath); diff --git a/packages/compiler/test/fixtures/fib-ir.ts b/packages/compiler/test/fixtures/fib-ir.ts index 3b5a43cbe..e33f2f7df 100644 --- a/packages/compiler/test/fixtures/fib-ir.ts +++ b/packages/compiler/test/fixtures/fib-ir.ts @@ -6,8 +6,7 @@ * } * console.log(fib(10)); // 55 * - * Exercises the IR (and later the C emitter) from the consumer side before - * any frontend exists. + * Exercises the IR and LLVM emitter directly, without the frontend. */ import type { IrExpr, IrModule, SrcLoc } from "../../src/ir/ir.js"; import { BOOL, F64, VOID } from "../../src/ir/ir.js"; diff --git a/packages/compiler/test/fromcharcode-emission.test.ts b/packages/compiler/test/fromcharcode-emission.test.ts index efaa28cdd..09110c4b8 100644 --- a/packages/compiler/test/fromcharcode-emission.test.ts +++ b/packages/compiler/test/fromcharcode-emission.test.ts @@ -1,10 +1,9 @@ import { expect, test } from "vitest"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { F64, STRING, VOID, arrayOf, type IrExpr, type IrModule } from "../src/ir/ir.js"; import { validateModule } from "../src/ir/validate.js"; -test("scalar fromCharCode avoids a heap argument pack in both backends", () => { +test("scalar fromCharCode avoids a heap argument pack in LLVM", () => { const loc = { file: "scalar-char.ts", start: 0, end: 0 }; const value: IrExpr = { kind: "numLit", value: 65, type: F64, loc }; const mod: IrModule = { @@ -18,10 +17,10 @@ test("scalar fromCharCode avoids a heap argument pack in both backends", () => { }], }; expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); + const llvm = emitLlvmModule(mod); - expect(c).toContain("scr_str_from_char_code_one("); + expect(llvm).toContain("call ptr @scr_str_from_char_code_one(double"); - expect(c).not.toContain("scr_arr_new("); + expect(llvm).not.toContain("call ptr @scr_arr_new("); }); diff --git a/packages/compiler/test/indexed-comparison.test.ts b/packages/compiler/test/indexed-comparison.test.ts index 32992384c..c2b576bc6 100644 --- a/packages/compiler/test/indexed-comparison.test.ts +++ b/packages/compiler/test/indexed-comparison.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; test("primitive indexed comparisons do not allocate optional-union boxes", async () => { @@ -40,10 +39,10 @@ test("primitive indexed comparisons do not allocate optional-union boxes", async } visitFunction(mod.entry); const reachable = { ...mod, functions: mod.functions.filter((f) => visited.has(f.name)) }; - const c = emitCModule(reachable), llvm = emitLlvmModule(reachable); - expect(c).not.toContain("scr_union_new_"); + const llvm = emitLlvmModule(reachable); + expect(llvm).not.toContain("@scr_union_new_"); - expect(c.match(/scr_arr_index_eq\(/g)).toHaveLength(3); + expect(llvm.match(/call zeroext i1 @scr_arr_index_eq\(/g)).toHaveLength(3); } finally { await rm(dir, { recursive: true, force: true }); diff --git a/packages/compiler/test/integer-bitwise-arithmetic.test.ts b/packages/compiler/test/integer-bitwise-arithmetic.test.ts index 3390e0182..bfdadfad2 100644 --- a/packages/compiler/test/integer-bitwise-arithmetic.test.ts +++ b/packages/compiler/test/integer-bitwise-arithmetic.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; test("bounded integer arithmetic emits integer operations and leaves uncertain numbers alone", async () => { @@ -25,16 +24,12 @@ console.log(safe(17), unknown(1.5), 1 / negativeZero()); const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const mod = deserializeModule(await readFile(outPath, "utf8")); - const c = emitCModule(mod); + const ll = emitLlvmModule(mod); - const cBody = (name: string): string => c.match(new RegExp(`static [^\\n]+ sc_f_${name}\\([^\\n]*\\) \\{([\\s\\S]*?)\\n\\}`))![1]!; const llBody = (name: string): string => ll.match(new RegExp(`define internal [^\\n]+ @sc_f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`))![1]!; - expect(cBody("safe").match(/scr_bit_/g)).toHaveLength(1); - expect(cBody("safe")).toContain("(double)((int64_t)"); expect(llBody("safe")).toContain(" = add nsw i54 "); // Only the arbitrary input's slow ToUint32 path still adds doubles. expect(llBody("safe").match(/ = fadd double /g)).toHaveLength(1); - expect(cBody("unknown")).toContain("scr_bit_ushr("); expect(llBody("unknown")).toContain("uint32.coerce.slow"); expect(llBody("unknown")).toContain(" = fadd double "); expect(llBody("negativeZero")).toContain(" = fadd double "); diff --git a/packages/compiler/test/internal-compiler-error.test.ts b/packages/compiler/test/internal-compiler-error.test.ts index 7359202ab..f24f87f7a 100644 --- a/packages/compiler/test/internal-compiler-error.test.ts +++ b/packages/compiler/test/internal-compiler-error.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { emitCModule, InternalCompilerError } from "../src/index.js"; +import { emitLlvmModule, InternalCompilerError } from "../src/index.js"; import { resolveCc } from "../src/backend/native-toolchain.js"; import { deserializeModule } from "../src/ir/serialize.js"; import { fibModule } from "./fixtures/fib-ir.js"; @@ -36,14 +36,14 @@ test("emitter invariant failures preserve their message and public type", () => kind: "closure", fnName: "missing", captures: [], - type: { kind: "func", params: [], returnType: { kind: "void" } }, + type: { kind: "func", params: [], ret: { kind: "void" } }, loc: statement.loc, }; - expect(() => emitCModule(malformed)).toThrow( + expect(() => emitLlvmModule(malformed)).toThrow( expect.objectContaining({ constructor: InternalCompilerError, - message: "emitter bug: closure over unknown function missing", + message: "llvm emitter bug: closure over unknown function missing", name: "InternalCompilerError", }), ); diff --git a/packages/compiler/test/js-override-returns.test.ts b/packages/compiler/test/js-override-returns.test.ts index d715dadb4..c4c2a4a25 100644 --- a/packages/compiler/test/js-override-returns.test.ts +++ b/packages/compiler/test/js-override-returns.test.ts @@ -7,7 +7,7 @@ import { analyze, compile } from "../src/index.js"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test.each(["c", "llvm"] as const)("incompatible JS return overrides refuse only when called (%s)", async (backend) => { +test.each(["llvm"] as const)("incompatible JS return overrides refuse only when called (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-override-returns-")); try { const entry = join(dir, "main.cjs"); diff --git a/packages/compiler/test/json-callbacks.test.ts b/packages/compiler/test/json-callbacks.test.ts index 683a0ec43..3d5ec2a55 100644 --- a/packages/compiler/test/json-callbacks.test.ts +++ b/packages/compiler/test/json-callbacks.test.ts @@ -29,7 +29,7 @@ test.each([ } }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`JSON callback checked boundaries and recovery (${backend})`, async () => { const dir = temp(); try { diff --git a/packages/compiler/test/library-identity-emission.test.ts b/packages/compiler/test/library-identity-emission.test.ts index 6112e94ea..417ad98e6 100644 --- a/packages/compiler/test/library-identity-emission.test.ts +++ b/packages/compiler/test/library-identity-emission.test.ts @@ -1,9 +1,7 @@ import { describe, expect, test } from "vitest"; -import { emitCModule } from "../src/index.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; -import { rebaseLibrarySourceComments, replaceLibraryIdentity, stripLibraryIdentity, stripLibrarySourceComments } from "../src/backend/library-identity-markers.js"; +import { replaceLibraryIdentity, stripLibraryIdentity } from "../src/backend/library-identity-markers.js"; import type { IrModule } from "../src/ir/ir.js"; -import { rebaseSourceLocations, createSourceLineRebaser } from "../src/library/semantic-source.js"; import { fibModule } from "./fixtures/fib-ir.js"; const libraryModule = (): IrModule => ({ @@ -28,14 +26,6 @@ const libraryModule = (): IrModule => ({ }); describe("library identity emission", () => { - test("direct C emission retains the IR-declared identity getters", () => { - const emitted = emitCModule(libraryModule()); - expect(emitted).toContain("uint64_t ie_build_id(void)"); - expect(emitted).toContain("return UINT64_C(0xfedcba9876543210);"); - expect(emitted).toContain("uint32_t ie_abi_version(void)"); - expect(emitted).toContain("return 7u;"); - }); - test("direct LLVM emission retains the IR-declared identity getters", () => { const emitted = emitLlvmModule(libraryModule()); expect(emitted).toContain("define i64 @ie_build_id()"); @@ -44,22 +34,19 @@ describe("library identity emission", () => { expect(emitted).toContain("ret i32 7"); }); - test("archive program-TU mode suppresses identity definitions in both backends", () => { + test("archive program-TU mode suppresses identity definitions in LLVM", () => { const mod = libraryModule(); - const c = emitCModule(mod, undefined, { emitLibraryIdentity: false }); + const llvm = emitLlvmModule(mod, { emitLibraryIdentity: false }); - expect(c).not.toContain("ie_build_id"); - expect(c).not.toContain("ie_abi_version"); + expect(llvm).not.toContain("@ie_build_id"); expect(llvm).not.toContain("@ie_abi_version"); - expect(stripLibraryIdentity(emitCModule(mod), "c")).toBe(c); - expect(stripLibraryIdentity(emitLlvmModule(mod), "llvm")).toBe(llvm); + + expect(stripLibraryIdentity(emitLlvmModule(mod))).toBe(llvm); mod.lib!.resultResetSymbol = "ie_reset"; - expect(stripLibraryIdentity(emitCModule(mod), "c")).toBe( - emitCModule(mod, undefined, { emitLibraryIdentity: false }), - ); - expect(stripLibraryIdentity(emitLlvmModule(mod), "llvm")).toBe( + + expect(stripLibraryIdentity(emitLlvmModule(mod))).toBe( emitLlvmModule(mod, { emitLibraryIdentity: false }), ); }); @@ -72,67 +59,8 @@ describe("library identity emission", () => { }; const expected = libraryModule(); expected.lib!.identity = identity; - expect(replaceLibraryIdentity(emitCModule(mod), "c", identity)).toBe(emitCModule(expected)); - expect(replaceLibraryIdentity(emitLlvmModule(mod), "llvm", identity)).toBe(emitLlvmModule(expected)); - }); - test("C source-line annotations can be refreshed and removed", () => { - const sourceFile = "/tmp/entry[1].ts"; - const emitted = [ - `value(); /* ${sourceFile}:3 */`, - `const char *text = " /* ${sourceFile}:5 */";`, - `slot = NULL; /* ${sourceFile}:7 */ /* let slot; */`, - `other(); /* ${sourceFile}:9 */`, - "", - ].join("\n"); - expect(rebaseLibrarySourceComments(emitted, sourceFile, (line) => line + 4)).toBe( - [ - `value(); /* ${sourceFile}:7 */`, - `const char *text = " /* ${sourceFile}:5 */";`, - `slot = NULL; /* ${sourceFile}:11 */ /* let slot; */`, - `other(); /* ${sourceFile}:13 */`, - "", - ].join("\n"), - ); - expect(stripLibrarySourceComments(emitted, sourceFile)).toBe( - `value();\nconst char *text = " /* ${sourceFile}:5 */";\nslot = NULL; /* let slot; */\nother();\n`, - ); + expect(replaceLibraryIdentity(emitLlvmModule(mod), identity)).toBe(emitLlvmModule(expected)); }); - test("cached C annotations match a fresh emission after a comment edit", () => { - const before = "// old note\nfunction fib() {\n return 1;\n}\n"; - const after = "/* longer\n * replacement note\n */\n\nfunction fib() {\n return 1;\n}\n"; - // JSON round-tripping gives every SrcLoc its own object, matching - // deserialized semantic-cache payloads rather than this fixture's shared - // hand-written `loc` constant. - const cachedMod = JSON.parse(JSON.stringify(libraryModule())) as IrModule; - const oldOffset = before.indexOf("return"); - const setLocations = (value: unknown): void => { - if (value === null || typeof value !== "object") return; - const record = value as Record; - if ( - record["file"] === "fib.ts" && - typeof record["start"] === "number" && - typeof record["end"] === "number" - ) { - record["start"] = oldOffset; - record["end"] = oldOffset + "return".length; - return; - } - Object.values(record).forEach(setLocations); - }; - setLocations(cachedMod); - const currentMod = structuredClone(cachedMod); - rebaseSourceLocations( - currentMod, - new Map([["fib.ts", before]]), - new Map([["fib.ts", after]]), - ); - const restored = rebaseLibrarySourceComments( - emitCModule(cachedMod, before), - "fib.ts", - createSourceLineRebaser("fib.ts", before, after), - ); - expect(restored).toBe(emitCModule(currentMod, after)); - }); }); diff --git a/packages/compiler/test/llvm-remainder.test.ts b/packages/compiler/test/llvm-remainder.test.ts index 12943cfe6..9d7919eef 100644 --- a/packages/compiler/test/llvm-remainder.test.ts +++ b/packages/compiler/test/llvm-remainder.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; test("numeric remainder emits strict LLVM frem and keeps exponentiation on pow", async () => { @@ -28,7 +27,5 @@ console.log(remainder(-9.5, 8), constants(17.25), power(2, 3)); expect(ll.match(/ = frem double /g)).toHaveLength(3); expect(ll).not.toContain("@fmod"); expect(ll).toContain("call double @pow("); - // The C backend remains on libm with the same operand expressions. - expect(emitCModule(mod).match(/fmod\(/g)).toHaveLength(3); } finally { await rm(dir, { recursive: true, force: true }); } }); diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index fac0d3eb9..d4f31e954 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -1,8 +1,7 @@ /* The LLVM backend's runtime ABI guard: every `declare` of a scr_* symbol * the emitter produces must agree with the C prototype in scr_runtime.h — * parameter count, integer width, pointer-ness, return type, and - * variadic-ness. The C backend gets this checked for free (clang - * type-checks its calls against the header), but a .ll `declare` is taken + * variadic-ness. A .ll `declare` is taken * on faith by the linker, so a disagreement is silent UB that can run * clean under one toolchain and misbehave under another. This test kills * the class mechanically, from four directions: @@ -361,7 +360,7 @@ _Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer off `${fixture} left the LLVM tier (${res.diagnostics[0]?.message ?? "?"}) — swap in an in-tier fs fixture`, ); } - const ll = await readFile(res.cPath, "utf8"); + const ll = await readFile(res.llvmPath, "utf8"); for (const line of ll.split("\n")) { if (!line.startsWith("declare ")) continue; const d = parseDeclare(line); diff --git a/packages/compiler/test/math-receiver-borrow.test.ts b/packages/compiler/test/math-receiver-borrow.test.ts index a3f020f4c..0a5b2a347 100644 --- a/packages/compiler/test/math-receiver-borrow.test.ts +++ b/packages/compiler/test/math-receiver-borrow.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; test("numeric Math operands borrow stable receivers but preserve snapshots across replacement and calls", async () => { @@ -30,30 +29,20 @@ replaced(buffer, new Uint8Array(1)); const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const mod = deserializeModule(await readFile(outPath, "utf8")); - const c = emitCModule(mod); + const ll = emitLlvmModule(mod); - const cBody = (name: string): string => { - const match = c.match(new RegExp(`static [^\\n]+ sc_f_${name}\\([^\\n]*\\) \\{([\\s\\S]*?)\\n\\}`)); - expect(match, `${name} C function`).not.toBeNull(); - return match![1]!; - }; const llBody = (name: string): string => { const match = ll.match(new RegExp(`define internal [^\\n]+ @sc_f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`)); expect(match, `${name} LLVM function`).not.toBeNull(); return match![1]!; }; - expect(cBody("safe")).not.toContain("scr_bytes_retain("); expect(llBody("safe")).not.toContain("@scr_bytes_retain_v("); // The incoming parameter still owns the buffer until function cleanup. - expect(cBody("safe").match(/scr_bytes_release\(/g)).toHaveLength(1); expect(llBody("safe").match(/call void @scr_bytes_release\(/g)).toHaveLength(1); - expect(cBody("safe")).toContain("floor("); expect(llBody("safe")).toContain("@llvm.floor.f64("); for (const name of ["owned", "replaced"]) { - expect(cBody(name)).toContain("scr_bytes_retain("); expect(llBody(name)).toContain("@scr_bytes_retain_v("); } - expect(cBody("readArray")).not.toContain("scr_arr_retain("); expect(llBody("readArray")).not.toContain("@scr_arr_retain_v("); } finally { await rm(dir, { recursive: true, force: true }); } }); diff --git a/packages/compiler/test/mobile-runtime-pack.test.ts b/packages/compiler/test/mobile-runtime-pack.test.ts new file mode 100644 index 000000000..fe565204c --- /dev/null +++ b/packages/compiler/test/mobile-runtime-pack.test.ts @@ -0,0 +1,56 @@ +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compileLibrary } from "../src/index.js"; + +const root = join(import.meta.dirname, "../../.."); +const dirs: string[] = []; +const savedTarget = process.env["SCRIPTC_TARGET"]; +const savedCompiler = process.env["SCRIPTC_CC"]; +afterEach(async () => { + if (savedTarget === undefined) delete process.env["SCRIPTC_TARGET"]; + else process.env["SCRIPTC_TARGET"] = savedTarget; + if (savedCompiler === undefined) delete process.env["SCRIPTC_CC"]; + else process.env["SCRIPTC_CC"] = savedCompiler; + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +for (const [target, pack, platform] of [ + ["aarch64-apple-ios", "ios-arm64", "IOS"], + ["aarch64-apple-ios-simulator", "ios-simulator-arm64", "IOSSIMULATOR"], + ["aarch64-linux-android", "android-arm64", "ELF"], +] as const) { + const available = existsSync(join(root, `packages/runtime-${pack}/runtime-pack.json`)) && + (platform === "ELF" || process.platform === "darwin"); + test.runIf(available).each(["release", "dev"] as const)(`${target} builds a localized per-thread library from packs (%s)`, async (optimization) => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-mobile-pack-")); + dirs.push(dir); + const fixture = join(root, "tests/library-mode/scalars"); + const profile = JSON.parse(await readFile(join(fixture, "profile.json"), "utf8")); + profile.entry = join(fixture, "lib.ts"); + profile.optimization = optimization; + profile.abi.localize_runtime = true; + profile.abi.instance_per_thread = true; + const profilePath = join(dir, "profile.json"); + await writeFile(profilePath, JSON.stringify(profile)); + process.env["SCRIPTC_TARGET"] = target; + process.env["SCRIPTC_CC"] = "/unavailable/compiler"; + const result = await compileLibrary({ profilePath, outDir: dir }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect((await readFile(result.archivePath)).subarray(0, 8).toString()).toBe("!\n"); + if (platform !== "ELF") { + const commands = execFileSync("otool", ["-l", result.archivePath], { encoding: "utf8" }); + expect(commands).toMatch(new RegExp(`platform (?:${platform}|${platform === "IOS" ? "2" : "7"})`)); + expect(commands).toMatch(/minos 15\.0/); + } else { + const members = execFileSync("zig", ["ar", "t", result.archivePath], { encoding: "utf8" }).trim().split("\n"); + expect(members).toHaveLength(1); + const object = execFileSync("zig", ["ar", "p", result.archivePath, members[0]!], { maxBuffer: 64 * 1024 * 1024 }); + expect(object.subarray(0, 4)).toEqual(Buffer.from([0x7f, 0x45, 0x4c, 0x46])); + expect(object.readUInt16LE(18)).toBe(183); // EM_AARCH64 + } + }); +} diff --git a/packages/compiler/test/native-callable-values.test.ts b/packages/compiler/test/native-callable-values.test.ts index 0802cfd3f..954b002c8 100644 --- a/packages/compiler/test/native-callable-values.test.ts +++ b/packages/compiler/test/native-callable-values.test.ts @@ -7,7 +7,7 @@ import { analyze, compile } from "../src/index.js"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test.each(["c", "llvm"] as const)("native callable checks preserve supported arguments and reject before invoking (%s)", async (backend) => { +test.each(["llvm"] as const)("native callable checks preserve supported arguments and reject before invoking (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-callable-checks-")); try { const entry = join(dir, "main.ts"); @@ -64,7 +64,7 @@ console.log(handle.fd); } }); -test.each(["c", "llvm"] as const)("failed implicit specializations retain a callable throwing body (%s)", async (backend) => { +test.each(["llvm"] as const)("failed implicit specializations retain a callable throwing body (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-failed-specialization-")); try { const pkg = join(dir, "node_modules", "callbacks"); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index ed567e60f..af6775394 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -65,7 +65,7 @@ describe.runIf(supported)("LLVM native helper integration", () => { expect(await readFile(output, "utf8")).toBe("existing\n"); await writeFile(input, "define i32 @answer() { ret i32 42 }\n"); - const unsupported = await run(helper, helperArgs(input, output, "x86_64-apple-macosx14.0.0")); + const unsupported = await run(helper, helperArgs(input, output, "riscv64-unknown-linux-gnu")); expect(JSON.parse(unsupported.stderr.toString("utf8"))).toMatchObject({ ok: false, code: "unsupported_target", diff --git a/packages/compiler/test/numeric-array-reads.test.ts b/packages/compiler/test/numeric-array-reads.test.ts index ae1077840..0464e063c 100644 --- a/packages/compiler/test/numeric-array-reads.test.ts +++ b/packages/compiler/test/numeric-array-reads.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; test("ordinary numeric-array arithmetic uses one lookup per read without boxing", async () => { @@ -45,13 +44,13 @@ test("ordinary numeric-array arithmetic uses one lookup per read without boxing" visitFunction(mod.entry); expect(arrayReads).toBe(3); const reachable = { ...mod, functions: mod.functions.filter((fn) => visited.has(fn.name)) }; - const c = emitCModule(reachable); + const llvm = emitLlvmModule(reachable); - expect(c.match(/scr_arr_get_number\(/g)).toHaveLength(3); + expect(llvm.match(/call double @scr_arr_get_number\(/g)).toHaveLength(3); - expect(c).not.toContain("scr_arr_state("); + expect(llvm).not.toContain("@scr_arr_state("); - expect(c).not.toContain("scr_arr_retain("); + expect(llvm).not.toContain("@scr_arr_retain_v"); } finally { await rm(dir, { recursive: true, force: true }); diff --git a/packages/compiler/test/process-builtin-values.test.ts b/packages/compiler/test/process-builtin-values.test.ts index 318b59fd3..64f1de999 100644 --- a/packages/compiler/test/process-builtin-values.test.ts +++ b/packages/compiler/test/process-builtin-values.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { compile } from "../src/index.js"; -test.each(["c", "llvm"] as const)("native builtin values refuse unimplemented modules and exports (%s)", async (backend) => { +test.each(["llvm"] as const)("native builtin values refuse unimplemented modules and exports (%s)", async (backend) => { const dir = mkdtempSync("/tmp/scriptc-builtin-values-"); try { const entry = join(dir, "main.js"); diff --git a/packages/compiler/test/scalar-record-emission.test.ts b/packages/compiler/test/scalar-record-emission.test.ts index 3c4ddd1f1..2d650b9bd 100644 --- a/packages/compiler/test/scalar-record-emission.test.ts +++ b/packages/compiler/test/scalar-record-emission.test.ts @@ -3,11 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; import { compile, deserializeModule, validateModule } from "../src/index.js"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { scalarizeNumericRecords } from "../src/ir/scalar-records.js"; -test("both backends scalarize ordinary typed calls while preserving producers with finally", async () => { +test("LLVM scalarize ordinary typed calls while preserving producers with finally", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-scalar-record-emission-")); try { const entry = join(dir, "main.ts"); @@ -38,11 +37,11 @@ render(); expect(validateModule(out)).toEqual([]); const render = out.functions.find((fn) => fn.name === "render")!; expect(render.locals.filter((l) => l.type.kind === "record").map((l) => l.name)).toEqual(["g"]); - const c = emitCModule(mod); + const llvm = emitLlvmModule(mod); - expect(c).not.toMatch(/= sc_f_pair\(/); + expect(llvm).not.toMatch(/call ptr @sc_f_pair\(/); - expect(c).toMatch(/= sc_f_guarded\(/); + expect(llvm).toMatch(/call ptr @sc_f_guarded\(/); } finally { await rm(dir, { recursive: true, force: true }); diff --git a/packages/compiler/test/source-debugging.test.ts b/packages/compiler/test/source-debugging.test.ts index 2391f5057..81bddb016 100644 --- a/packages/compiler/test/source-debugging.test.ts +++ b/packages/compiler/test/source-debugging.test.ts @@ -12,7 +12,7 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1"; // Xcode ships this debugger. Resolve real breakpoints after temporary native // objects have been deleted, including after the executable cache restores. -test.runIf(process.platform === "darwin").each(["llvm", "c"] as const)( +test.runIf(process.platform === "darwin").each(["llvm"] as const)( "dev %s builds preserve source breakpoints across cache hits", async (backend) => { const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-")); @@ -98,7 +98,7 @@ test.each([ try { const entry = join(corpus, "..", fixture); const node = await exec(process.execPath, [entry]); - for (const backend of ["c", "llvm"] as const) { + for (const backend of ["llvm"] as const) { const outPath = join(dir, process.platform === "win32" ? `${backend}.exe` : backend); const built = await compile(entry, { outDir: dir, outPath, backend, optimization: "dev", sanitize }); expect(built.ok, !built.ok ? JSON.stringify(built.diagnostics) : "").toBe(true); diff --git a/packages/compiler/test/source-output.test.ts b/packages/compiler/test/source-output.test.ts index 157dafc5c..1cd18f146 100644 --- a/packages/compiler/test/source-output.test.ts +++ b/packages/compiler/test/source-output.test.ts @@ -39,20 +39,14 @@ test("Windows subsystem selection refuses non-executable compiler output", async await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" }); }); -test("C and LLVM are exact primary artifacts and never create an executable", async () => { +test("LLVM is the exact primary artifact and never creates an executable", async () => { const { entry, outDir } = await fixture(); - const cPath = join(outDir, "exact.output"); - const c = await compile(entry, { outDir, outPath: cPath, outputKind: "c" }); - if (!c.ok) throw new Error("C emission failed"); - expect(c.artifact).toEqual({ kind: "c", path: cPath }); - expect(await readFile(cPath, "utf8")).toContain("Generated by scriptc"); - const llvmPath = join(outDir, "exact.llvm-output"); const llvm = await compile(entry, { outDir, outPath: llvmPath, outputKind: "llvm" }); if (!llvm.ok) throw new Error("LLVM emission failed"); expect(llvm.artifact).toEqual({ kind: "llvm", path: llvmPath }); expect(await readFile(llvmPath, "utf8")).toContain("define i32 @main"); - expect((await readdir(outDir)).sort()).toEqual(["exact.llvm-output", "exact.output"]); + expect((await readdir(outDir)).sort()).toEqual(["exact.llvm-output"]); }); test("switching default source output kinds preserves earlier generated siblings", async () => { @@ -62,7 +56,6 @@ test("switching default source output kinds preserves earlier generated siblings await writeFile(join(outDir, executable), "saved executable"); const artifacts = [executable]; for (const [kind, name] of [ - ["c", "main.c"], ["llvm", "main.ll"], ["ir", "main.ir.json"], ] as const) { @@ -108,7 +101,7 @@ test("an executable build never deletes same-stem assembly or object artifacts", const result = await compile(entry, { outDir, outPath: join(outDir, "custom-executable"), - backend: "c", + backend: "llvm", }); if (!result.ok) throw new Error("executable build failed"); for (const path of siblings) { @@ -116,45 +109,24 @@ test("an executable build never deletes same-stem assembly or object artifacts", } }); -test.each([undefined, "c"] as const)( - "native program-object validation rejects backend %s as a diagnostic", - async (backend) => { - const { entry, outDir } = await fixture(); - const result = await compile(entry, { - outDir, - outPath: join(outDir, "program"), - ...(backend === undefined ? {} : { backend }), - nativeProgramObject: true, - }); - expect(result).toMatchObject({ - ok: false, - diagnostics: [{ code: "SC3002", message: expect.stringContaining("backend explicitly set to llvm") }], - }); - await expect(readdir(outDir)).rejects.toMatchObject({ code: "ENOENT" }); - }, -); +test.each([{ backend: "c" }, { outputKind: "c" }])("removed C options reject before creating output: %s", async (removed) => { + const { entry, outDir } = await fixture(); + // JavaScript callers may supply flags that are absent from the TypeScript API. + const result = await compile(entry, { outDir, ...removed } as never); + expect(result).toMatchObject({ ok: false, diagnostics: [{ code: "SC3002", message: expect.stringContaining("LLVM is the only backend") }] }); + await expect(readdir(outDir)).rejects.toMatchObject({ code: "ENOENT" }); +}); -test("explicit LLVM output refuses unsupported programs with SC3001", async () => { - const dir = join(import.meta.dirname, "../../../tests/fixtures/server/cases/tls-connect-basic"); - const entry = join(dir, "main.ts"); - const outDir = await mkdtemp(join(tmpdir(), "scriptc-source-refusal-")); +test("TLS callback programs emit LLVM without a secondary backend", async () => { + const entry = join(import.meta.dirname, "../../../tests/fixtures/server/cases/tls-connect-basic/main.ts"); + const outDir = await mkdtemp(join(tmpdir(), "scriptc-source-tls-")); dirs.push(outDir); - const result = await compile(entry, { - outDir, - outPath: join(outDir, "main.ll"), - outputKind: "llvm", - }); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(result.diagnostics.map(({ code, message }) => ({ code, message }))).toMatchInlineSnapshot(` - [ - { - "code": "SC3001", - "message": "the LLVM backend does not support this construct yet (libCall:tls.connectCb); build with the debugging C backend (--backend c, or the native default's transparent fallback)", - }, - ] - `); - expect(await readdir(outDir)).toEqual([]); + const outPath = join(outDir, "main.ll"); + const result = await compile(entry, { outDir, outPath, outputKind: "llvm" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + expect(result.artifact).toEqual({ kind: "llvm", path: outPath }); + expect(await readFile(outPath, "utf8")).toContain("@scr_tls_connect"); + expect(await readdir(outDir)).toEqual(["main.ll"]); }); test("source outputs ignore invalid external compiler selection and create no executable cache", async () => { @@ -167,8 +139,7 @@ test("source outputs ignore invalid external compiler selection and create no ex try { for (const [kind, name] of [ ["ir", "main.ir.json"], - ["c", "main.c"], - ["llvm", "main.ll"], + ["llvm", "main.ll"], ] as const) { const result = await compile(entry, { outDir, diff --git a/packages/compiler/test/string-accumulation-emission.test.ts b/packages/compiler/test/string-accumulation-emission.test.ts index 32b8c4250..5af41ff8e 100644 --- a/packages/compiler/test/string-accumulation-emission.test.ts +++ b/packages/compiler/test/string-accumulation-emission.test.ts @@ -1,5 +1,4 @@ import { expect, test } from "vitest"; -import { emitCModule } from "../src/backend/c/c-emitter.js"; import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { STRING, VOID, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrStmt } from "../src/ir/ir.js"; import { validateModule } from "../src/ir/validate.js"; @@ -75,18 +74,13 @@ function expectInOrder(text: string, fragments: readonly string[]): void { } } -test("C and LLVM hand off canonical string self-concats after suffix evaluation", () => { +test("LLVM hands off canonical string self-concats after suffix evaluation", () => { const mod = fixture(); expect(validateModule(mod)).toEqual([]); - const c = emitCModule(mod); + const llvm = emitLlvmModule(mod); // Plain local: retained snapshot, suffix, detach/release, concat, move. - expectInOrder(c, [ - "scr_str_retain(sc_l_acc)", - "ScrStr *sc_t", "= sc_l_acc;", "sc_l_acc = NULL;", "scr_str_release(sc_t", - "scr_str_concat(sc_t", "sc_l_acc = sc_t", - ]); expectInOrder(llvm, [ "call ptr @scr_str_retain_v(ptr %t", "load ptr, ptr %sc_l_acc", "store ptr null, ptr %sc_l_acc", @@ -94,26 +88,29 @@ test("C and LLVM hand off canonical string self-concats after suffix evaluation" ]); // Module globals use the same plain-slot handoff; boxes use set_ref(NULL). - expect(c).toContain("sc_g_e_acc = NULL;"); + expect(llvm).toContain("store ptr null, ptr @sc_g_e_acc"); - expect(c).toContain("scr_box_set_ref(sc_l_boxed, NULL);"); + expect(llvm).toContain("call void @scr_box_set_ref(ptr %"); // The expression form leaves its own result live and gives the binding a // retained sibling. A suffix assignment changes the binding before the // final detach, so the detach must occur after its replacement store. - expect(c).toMatch(/scr_str_concat\(sc_t\d+, sc_t\d+\);\n\s*scr_box_set_ref|scr_str_concat\(sc_t\d+, sc_t\d+\);\n\s*sc_l_acc = scr_str_retain/); - const replacementStore = c.indexOf("sc_l_acc = scr_str_retain(sc_t"); - const postSuffixDetach = c.indexOf("sc_l_acc = NULL;", replacementStore); - expect(replacementStore).toBeGreaterThanOrEqual(0); - expect(postSuffixDetach).toBeGreaterThan(replacementStore); - // A different left binding keeps the generic concat lowering: it never - // clears the destination before invoking concat. - const negativeStart = c.indexOf("static void sc_f_negative(void) {"); - const negativeEnd = c.indexOf("}", negativeStart); - const negative = c.slice(negativeStart, negativeEnd); - expect(negativeStart).toBeGreaterThanOrEqual(0); - expect(negative).toContain("scr_str_concat("); - expect(negative).not.toMatch(/sc_l_acc = NULL;\n\s*scr_str_release\(sc_t\d+\);\n\s*ScrStr \*sc_t\d+ = scr_str_concat/); + const suffixBody = llvm.match(/define internal void @sc_f_suffixReassign\([^]*?\n\}/)?.[0]; + expect(suffixBody).toBeDefined(); + expectInOrder(suffixBody!, [ + "call ptr @scr_str_retain_v", "store ptr %", "ptr %sc_l_acc", + "load ptr, ptr %sc_l_acc", "store ptr null, ptr %sc_l_acc", + "call void @scr_str_release", "call ptr @scr_str_concat", + ]); + + // Concatenating another binding keeps the destination live until concat. + const negative = llvm.match(/define internal void @sc_f_negative\([^]*?\n\}/)?.[0]; + expect(negative).toBeDefined(); + expect(negative).toContain("call ptr @scr_str_concat"); + const concat = negative!.indexOf("call ptr @scr_str_concat"); + const initialized = negative!.match(/store ptr %\w+, ptr %sc_l_acc/); + expect(initialized).not.toBeNull(); + expect(negative!.slice(initialized!.index!, concat)).not.toContain("store ptr null, ptr %sc_l_acc"); }); diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 0d89c4727..cb04950fe 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -11673,6 +11673,36 @@ "/tests/corpus/process-uncaught-sync.cjs" ], "diags": [] + }, + "/tests/corpus/4030-runtime-object-truthiness.ts": { + "order": [ + "/tests/corpus/4030-runtime-object-truthiness.ts" + ], + "diags": [] + }, + "/tests/corpus/4031-event-emitter-long-tuples.ts": { + "order": [ + "/tests/corpus/4031-event-emitter-long-tuples.ts" + ], + "diags": [] + }, + "/tests/corpus/logical-runtime-optional-record.ts": { + "order": [ + "/tests/corpus/logical-runtime-optional-record.ts" + ], + "diags": [] + }, + "/tests/corpus/union-nested-layout-discriminant.ts": { + "order": [ + "/tests/corpus/union-nested-layout-discriminant.ts" + ], + "diags": [] + }, + "/tests/corpus/llvm-read-receiver-lifetime.ts": { + "order": [ + "/tests/corpus/llvm-read-receiver-lifetime.ts" + ], + "diags": [] } } } diff --git a/packages/llvm-darwin-arm64/scripts/build.mjs b/packages/llvm-darwin-arm64/scripts/build.mjs index 3c64b8451..8361c471c 100644 --- a/packages/llvm-darwin-arm64/scripts/build.mjs +++ b/packages/llvm-darwin-arm64/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -20,8 +21,7 @@ execFileSync("cmake", [ "-G", "Ninja", `-DLLVM_DIR=${llvmDir}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, - "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", - "-DSCRIPTC_ALLOWED_TARGETS=arm64-apple-macosx14.0.0,wasm32-unknown-wasi", + ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release", ], { stdio: "inherit" }); execFileSync("cmake", ["--build", buildDir, "--target", "scriptc-llvm-codegen"], { diff --git a/packages/llvm-darwin-x64/scripts/build.mjs b/packages/llvm-darwin-x64/scripts/build.mjs index 0f4ddea72..a9a9e4ffd 100644 --- a/packages/llvm-darwin-x64/scripts/build.mjs +++ b/packages/llvm-darwin-x64/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -12,7 +13,7 @@ if (process.platform !== "darwin" || process.arch !== "x64") { } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-darwin-x64"); -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/local/opt/llvm@22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_HELPER_ARCH=x86_64", "-DSCRIPTC_DEFAULT_TARGET=x86_64-apple-macosx14.0.0", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-apple-macosx14.0.0,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/local/opt/llvm@22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_HELPER_ARCH=x86_64", "-DSCRIPTC_DEFAULT_TARGET=x86_64-apple-macosx14.0.0", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); diff --git a/packages/llvm-linux-arm64-gnu/scripts/build.mjs b/packages/llvm-linux-arm64-gnu/scripts/build.mjs index 3a2e631b2..515a2e61a 100644 --- a/packages/llvm-linux-arm64-gnu/scripts/build.mjs +++ b/packages/llvm-linux-arm64-gnu/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -6,5 +7,5 @@ import { fileURLToPath } from "node:url"; const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = fileURLToPath(new URL("../../..", import.meta.url)); if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/llvm-linux-arm64-gnu: skipped on this host\n"); process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-gnu"); -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=aarch64-unknown-linux-gnu,aarch64-unknown-linux-musl,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755); diff --git a/packages/llvm-linux-arm64-musl/scripts/build.mjs b/packages/llvm-linux-arm64-musl/scripts/build.mjs index 5869d7438..87f832c47 100644 --- a/packages/llvm-linux-arm64-musl/scripts/build.mjs +++ b/packages/llvm-linux-arm64-musl/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -7,5 +8,5 @@ const root = dirname(dirname(fileURLToPath(import.meta.url))); const repo = file if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/llvm-linux-arm64-musl: skipped on this host\n"); process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-arm64-musl"); -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", "-DSCRIPTC_TARGET_BACKENDS=AArch64;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=aarch64-unknown-linux-musl,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=aarch64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755); diff --git a/packages/llvm-linux-x64-gnu/scripts/build.mjs b/packages/llvm-linux-x64-gnu/scripts/build.mjs index 2c38d8641..02d55dda4 100644 --- a/packages/llvm-linux-x64-gnu/scripts/build.mjs +++ b/packages/llvm-linux-x64-gnu/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -9,7 +10,7 @@ const repo = fileURLToPath(new URL("../../..", import.meta.url)); if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/llvm-linux-x64-gnu: skipped on this host\n"); process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-gnu"); -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-unknown-linux-gnu,x86_64-unknown-linux-musl,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-gnu", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755); diff --git a/packages/llvm-linux-x64-musl/scripts/build.mjs b/packages/llvm-linux-x64-musl/scripts/build.mjs index 7f57a5dd9..03a217d82 100644 --- a/packages/llvm-linux-x64-musl/scripts/build.mjs +++ b/packages/llvm-linux-x64-musl/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -9,5 +10,5 @@ const repo = fileURLToPath(new URL("../../..", import.meta.url)); if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/llvm-linux-x64-musl: skipped on this host\n"); process.exit(0); } const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const build = join(repo, "node_modules/.cache/scriptc-llvm-linux-x64-musl"); -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-unknown-linux-musl,wasm32-unknown-wasi", "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, "-G", "Ninja", `-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`, `-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`, "-DSCRIPTC_MUSL_BUILD=ON", "-DCMAKE_EXE_LINKER_FLAGS=-static", `-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-unknown-linux-musl", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs, "-DCMAKE_BUILD_TYPE=Release"], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); const output = join(root, "bin", "scriptc-llvm-codegen"); copyFileSync(join(build, "scriptc-llvm-codegen"), output); execFileSync("strip", [output], { stdio: "inherit" }); chmodSync(output, 0o755); diff --git a/packages/llvm-win32-x64-msvc/scripts/build.mjs b/packages/llvm-win32-x64-msvc/scripts/build.mjs index b6ba8ce17..034196482 100644 --- a/packages/llvm-win32-x64-msvc/scripts/build.mjs +++ b/packages/llvm-win32-x64-msvc/scripts/build.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { nativeTargetCmakeArgs } from "../../../native/llvm-codegen/targets.mjs"; import { execFileSync } from "node:child_process"; import { copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -13,5 +14,5 @@ const output = cmakeReleaseOutput(build); // therefore the matching CRT, SDK, manifest tools, and MSVC standard library. // CMAKE_GENERATOR is an advanced override whose environment must provide an // ABI-compatible Windows compiler and SDK when it selects a non-VS generator. -execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, ...cmakeGeneratorArgs(), ...output.configureArgs, `-DLLVM_DIR=${process.env.LLVM_DIR ?? "C:/Program Files/LLVM/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-pc-windows-msvc", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "-DSCRIPTC_TARGET_BACKENDS=X86;WebAssembly", "-DSCRIPTC_ALLOWED_TARGETS=x86_64-pc-windows-msvc,wasm32-unknown-wasi"], { stdio: "inherit" }); +execFileSync("cmake", ["-S", join(repo, "native/llvm-codegen"), "-B", build, ...cmakeGeneratorArgs(), ...output.configureArgs, `-DLLVM_DIR=${process.env.LLVM_DIR ?? "C:/Program Files/LLVM/lib/cmake/llvm"}`, `-DSCRIPTC_PACKAGE_VERSION=${manifest.version}`, "-DSCRIPTC_DEFAULT_TARGET=x86_64-pc-windows-msvc", "-DSCRIPTC_DEFAULT_DATA_LAYOUT=e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", ...nativeTargetCmakeArgs], { stdio: "inherit" }); execFileSync("cmake", ["--build", build, "--config", "Release", "--target", "scriptc-llvm-codegen"], { stdio: "inherit" }); mkdirSync(join(root, "bin"), { recursive: true }); copyFileSync(output.executable, join(root, "bin", "scriptc-llvm-codegen.exe")); diff --git a/packages/llvm-win32-x64-msvc/test/build.test.ts b/packages/llvm-win32-x64-msvc/test/build.test.ts new file mode 100644 index 000000000..4e5f60439 --- /dev/null +++ b/packages/llvm-win32-x64-msvc/test/build.test.ts @@ -0,0 +1,26 @@ +import { afterEach, expect, test, vi } from "vitest"; + +const commands = vi.hoisted(() => vi.fn()); +vi.mock("node:child_process", () => ({ execFileSync: commands })); +vi.mock("node:fs", () => ({ + copyFileSync: vi.fn(), mkdirSync: vi.fn(), + readFileSync: () => JSON.stringify({ version: "0.1.7" }), +})); + +afterEach(() => { vi.unstubAllGlobals(); }); + +test("the Windows build enables every production object target", async () => { + vi.stubGlobal("process", { ...process, platform: "win32", arch: "x64", env: {} }); + await import("../scripts/build.mjs"); + const configure = commands.mock.calls.find(([, args]) => args.includes("-S")); + expect(configure?.[0]).toBe("cmake"); + const args = configure![1] as string[]; + expect(args.filter((arg) => arg.startsWith("-DSCRIPTC_TARGET_BACKENDS="))) + .toEqual(["-DSCRIPTC_TARGET_BACKENDS=AArch64;X86;WebAssembly"]); + const targets = args.filter((arg) => arg.startsWith("-DSCRIPTC_ALLOWED_TARGETS=")); + expect(targets).toHaveLength(1); + expect(targets[0]!.split("=")[1]!.split(",")).toEqual(expect.arrayContaining([ + "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", + "wasm32-unknown-wasi", "arm64-apple-ios15.0.0", "aarch64-unknown-linux-android26", + ])); +}); diff --git a/packages/runtime-android-arm64/LICENSE b/packages/runtime-android-arm64/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/packages/runtime-android-arm64/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json new file mode 100644 index 000000000..1190be842 --- /dev/null +++ b/packages/runtime-android-arm64/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scriptc/runtime-android-arm64", + "version": "0.1.7", + "description": "Precompiled scriptc library runtime pack for Android arm64", + "license": "Apache-2.0", + "homepage": "https://scriptc.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/scriptc.git", + "directory": "packages/runtime-android-arm64" + }, + "files": [ + "artifacts", + "runtime-pack.json" + ], + "scripts": { + "build": "true", + "build:native": "node scripts/build.mjs", + "prepack": "node scripts/verify.mjs" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/runtime-android-arm64/scripts/build.mjs b/packages/runtime-android-arm64/scripts/build.mjs new file mode 100644 index 000000000..425e7aa28 --- /dev/null +++ b/packages/runtime-android-arm64/scripts/build.mjs @@ -0,0 +1,7 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { mobileRuntimeConfig } from "../../runtime-pack-common/scripts/mobile-toolchain.mjs"; + +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify(mobileRuntimeConfig("android-arm64")); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-android-arm64/scripts/verify.mjs b/packages/runtime-android-arm64/scripts/verify.mjs new file mode 100644 index 000000000..e5bea86c4 --- /dev/null +++ b/packages/runtime-android-arm64/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index 29b1a61eb..586152ec4 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-darwin-arm64" }, - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index d890889cb..ecd74262a 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -1,3 +1,5 @@ +import { withLibraryRuntimeFlavors } from "../runtime-pack-common/library-matrix.mjs"; + /** * The executable runtime-pack matrix. This is the single source of truth for * both release compilation and the generated feature predicates consumed by @@ -91,7 +93,7 @@ function variantsFor(source) { return variants; } -export const RUNTIME_PACK_MATRIX = { +export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors({ schema: "scriptc.runtime-pack-matrix.v1", target: { name: "macos-arm64", @@ -113,4 +115,4 @@ export const RUNTIME_PACK_MATRIX = { { id: "mbedtls", predicate: "tlsEffective" }, ], system_libraries: [{ name: "System", predicate: true }, { name: "m", predicate: "dynamic" }], -}; +}); diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 33cd428e2..2bfaacd8d 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -21,7 +21,7 @@ const vendorRoot = join(runtimeRoot, "vendor"); const outputRoot = join(packageRoot, "artifacts"); const manifestPath = join(packageRoot, "runtime-pack.json"); -if (process.platform !== "darwin" || process.arch !== "arm64") { +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "darwin" || process.arch !== "arm64")) { process.stdout.write("@scriptc/runtime-darwin-arm64: skipped on this host\n"); process.exit(0); } @@ -101,9 +101,10 @@ async function build() { const flavors = {}; for (const [flavor, flavorSpec] of Object.entries(RUNTIME_PACK_MATRIX.flavors)) { const units = []; - for (const unit of RUNTIME_PACK_MATRIX.runtime_units) { + for (const unit of flavorSpec.runtime_units ?? RUNTIME_PACK_MATRIX.runtime_units) { const variants = []; - for (const variant of unit.variants) { + for (const baseVariant of unit.variants) { + const variant = { ...baseVariant, defines: [...(flavorSpec.defines ?? []), ...baseVariant.defines] }; const variantName = variant.id === "default" ? "default" : variant.id; const output = join( stagedOutputRoot, @@ -188,7 +189,7 @@ async function build() { }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], - excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], + excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, flavors, diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index e451529e4..98179ef2f 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-darwin-x64" }, - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-darwin-x64/scripts/build.mjs b/packages/runtime-darwin-x64/scripts/build.mjs index 029350d97..5cfb22c31 100644 --- a/packages/runtime-darwin-x64/scripts/build.mjs +++ b/packages/runtime-darwin-x64/scripts/build.mjs @@ -1,5 +1,5 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -if (process.platform !== "darwin" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-darwin-x64: skipped on this host\n"); process.exit(0); } +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "darwin" || process.arch !== "x64")) { process.stdout.write("@scriptc/runtime-darwin-x64: skipped on this host\n"); process.exit(0); } process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "darwin", runtimeDefines: [], threadArgs: ["-pthread"], target: { name: "macos-x64", llvm_triple: "x86_64-apple-macosx14.0.0", architecture: "x64", object_format: "macho", minimum_os: "14.0" }, targetArgs: ["-target", "x86_64-apple-macosx14.0.0"], compileFlags: [], systemLibraries: [{ name: "System", predicate: true }, { name: "m", predicate: "dynamic" }] }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-ios-arm64/LICENSE b/packages/runtime-ios-arm64/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/packages/runtime-ios-arm64/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json new file mode 100644 index 000000000..e2a57493d --- /dev/null +++ b/packages/runtime-ios-arm64/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scriptc/runtime-ios-arm64", + "version": "0.1.7", + "description": "Precompiled scriptc library runtime pack for iOS arm64", + "license": "Apache-2.0", + "homepage": "https://scriptc.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/scriptc.git", + "directory": "packages/runtime-ios-arm64" + }, + "files": [ + "artifacts", + "runtime-pack.json" + ], + "scripts": { + "build": "true", + "build:native": "node scripts/build.mjs", + "prepack": "node scripts/verify.mjs" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/runtime-ios-arm64/scripts/build.mjs b/packages/runtime-ios-arm64/scripts/build.mjs new file mode 100644 index 000000000..d81f9803f --- /dev/null +++ b/packages/runtime-ios-arm64/scripts/build.mjs @@ -0,0 +1,7 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { mobileRuntimeConfig } from "../../runtime-pack-common/scripts/mobile-toolchain.mjs"; + +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify(mobileRuntimeConfig("ios-arm64")); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-ios-arm64/scripts/verify.mjs b/packages/runtime-ios-arm64/scripts/verify.mjs new file mode 100644 index 000000000..e5bea86c4 --- /dev/null +++ b/packages/runtime-ios-arm64/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-ios-simulator-arm64/LICENSE b/packages/runtime-ios-simulator-arm64/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/packages/runtime-ios-simulator-arm64/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json new file mode 100644 index 000000000..328ac6160 --- /dev/null +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -0,0 +1,24 @@ +{ + "name": "@scriptc/runtime-ios-simulator-arm64", + "version": "0.1.7", + "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", + "license": "Apache-2.0", + "homepage": "https://scriptc.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/scriptc.git", + "directory": "packages/runtime-ios-simulator-arm64" + }, + "files": [ + "artifacts", + "runtime-pack.json" + ], + "scripts": { + "build": "true", + "build:native": "node scripts/build.mjs", + "prepack": "node scripts/verify.mjs" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/runtime-ios-simulator-arm64/scripts/build.mjs b/packages/runtime-ios-simulator-arm64/scripts/build.mjs new file mode 100644 index 000000000..dcae156f4 --- /dev/null +++ b/packages/runtime-ios-simulator-arm64/scripts/build.mjs @@ -0,0 +1,7 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { mobileRuntimeConfig } from "../../runtime-pack-common/scripts/mobile-toolchain.mjs"; + +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); +process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify(mobileRuntimeConfig("ios-simulator-arm64")); +await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-ios-simulator-arm64/scripts/verify.mjs b/packages/runtime-ios-simulator-arm64/scripts/verify.mjs new file mode 100644 index 000000000..e5bea86c4 --- /dev/null +++ b/packages/runtime-ios-simulator-arm64/scripts/verify.mjs @@ -0,0 +1,2 @@ +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); await import("../../runtime-pack-common/scripts/verify.mjs"); diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index faff5aa2a..d46cde9d8 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-arm64-gnu" }, - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-linux-arm64-gnu/scripts/build.mjs b/packages/runtime-linux-arm64-gnu/scripts/build.mjs index 7ee089c8d..2f9b4838d 100644 --- a/packages/runtime-linux-arm64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-arm64-gnu/scripts/build.mjs @@ -2,7 +2,7 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { glibcRuntimeToolchain } from "../../runtime-pack-common/scripts/glibc-toolchain.mjs"; -if (process.platform !== "linux" || process.arch !== "arm64") { +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "linux" || process.arch !== "arm64")) { process.stdout.write("@scriptc/runtime-linux-arm64-gnu: skipped on this host\n"); process.exit(0); } diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 429ee92dd..e981c7d77 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-arm64-musl" }, - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-linux-arm64-musl/scripts/build.mjs b/packages/runtime-linux-arm64-musl/scripts/build.mjs index 10fa626d9..51d12c64a 100644 --- a/packages/runtime-linux-arm64-musl/scripts/build.mjs +++ b/packages/runtime-linux-arm64-musl/scripts/build.mjs @@ -1,5 +1,5 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; -if (process.platform !== "linux" || process.arch !== "arm64") { process.stdout.write("@scriptc/runtime-linux-arm64-musl: skipped on this host\n"); process.exit(0); } +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "linux" || process.arch !== "arm64")) { process.stdout.write("@scriptc/runtime-linux-arm64-musl: skipped on this host\n"); process.exit(0); } process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE", "SCR_MUSL"], threadArgs: ["-pthread"], extraRuntimeSources: ["scr_musl.c"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "linux-arm64-musl", llvm_triple: "aarch64-unknown-linux-musl", architecture: "arm64", object_format: "elf", minimum_os: "musl 1.2" }, targetArgs: ["-target", "aarch64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index 025afe25f..0b32575b7 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-x64-gnu" }, - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-linux-x64-gnu/scripts/build.mjs b/packages/runtime-linux-x64-gnu/scripts/build.mjs index 7d226dabd..9ae641a87 100644 --- a/packages/runtime-linux-x64-gnu/scripts/build.mjs +++ b/packages/runtime-linux-x64-gnu/scripts/build.mjs @@ -2,7 +2,7 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { glibcRuntimeToolchain } from "../../runtime-pack-common/scripts/glibc-toolchain.mjs"; -if (process.platform !== "linux" || process.arch !== "x64") { +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "linux" || process.arch !== "x64")) { process.stdout.write("@scriptc/runtime-linux-x64-gnu: skipped on this host\n"); process.exit(0); } diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index a7e8355f9..a1d768866 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-linux-x64-musl" }, - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-linux-x64-musl/scripts/build.mjs b/packages/runtime-linux-x64-musl/scripts/build.mjs index 7897aa169..f44c97653 100644 --- a/packages/runtime-linux-x64-musl/scripts/build.mjs +++ b/packages/runtime-linux-x64-musl/scripts/build.mjs @@ -1,5 +1,5 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; -if (process.platform !== "linux" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-linux-x64-musl: skipped on this host\n"); process.exit(0); } +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "linux" || process.arch !== "x64")) { process.stdout.write("@scriptc/runtime-linux-x64-musl: skipped on this host\n"); process.exit(0); } process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "linux", runtimeDefines: ["_GNU_SOURCE", "SCR_MUSL"], threadArgs: ["-pthread"], extraRuntimeSources: ["scr_musl.c"], compiler: "zig", compilerArgs: ["cc"], archiver: "zig", archiverArgs: ["ar"], target: { name: "linux-x64-musl", llvm_triple: "x86_64-unknown-linux-musl", architecture: "x64", object_format: "elf", minimum_os: "musl 1.2" }, targetArgs: ["-target", "x86_64-linux-musl"], compileFlags: ["-ffunction-sections", "-fdata-sections"], systemLibraries: [{ name: "m", predicate: true }] }); await import("../../runtime-pack-common/scripts/build.mjs"); diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs new file mode 100644 index 000000000..45b3719f1 --- /dev/null +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -0,0 +1,23 @@ +/** Runtime library modes have distinct trap and state ownership contracts. */ +export function withLibraryRuntimeFlavors(matrix) { + const omitted = new Set(["scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_ffi.c"]); + const optional = new Set([ + "scr_regex.c", "scr_assert.c", "scr_inspect.c", "scr_symbol.c", "scr_bigint_assert.c", + "scr_url_params.c", "scr_events_emitter.c", "scr_dyn_handle.c", "scr_zlib.c", "scr_copying.c", + ]); + const libraryUnits = [ + ...matrix.runtime_units.filter((unit) => !omitted.has(unit.source) && (unit.predicate === true || optional.has(unit.source))), + { source: "scr_library.c", predicate: true, variants: [{ id: "default", when: {}, defines: [] }] }, + ].map((unit) => ({ + ...unit, + variants: unit.variants.filter((variant) => !variant.defines.includes("SCR_DYNAMIC")), + })); + const flavors = { ...matrix.flavors }; + if (matrix.target.object_format !== "wasm") { + for (const [flavor, spec] of Object.entries(matrix.flavors)) { + flavors[`library-${flavor}`] = { ...spec, defines: ["SCR_LIB"], runtime_units: libraryUnits }; + flavors[`library-thread-${flavor}`] = { ...spec, defines: ["SCR_LIB", "SCR_THREAD_INSTANCES"], runtime_units: libraryUnits }; + } + } + return { ...matrix, flavors }; +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6df199289..cae021c7e 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -1,3 +1,5 @@ +import { withLibraryRuntimeFlavors } from "./library-matrix.mjs"; + /** * Shared release-runtime pack matrix. Platform package scripts provide the * target descriptor, then this module fixes feature reachability and the @@ -67,6 +69,7 @@ export function createRuntimePackMatrix({ omitRuntimeSources = [], omitOptionalSources = [], omitArchives = [], + libraryOnly = false, }) { const vendorArchives = [ { id: "quickjs", predicate: "dynamic" }, @@ -74,7 +77,7 @@ export function createRuntimePackMatrix({ { id: "zlib", predicate: "zlibEffective" }, { id: "mbedtls", predicate: "tlsEffective" }, ]; - return { + const matrix = withLibraryRuntimeFlavors({ schema: "scriptc.runtime-pack-matrix.v1", target, flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } }, @@ -90,5 +93,10 @@ export function createRuntimePackMatrix({ ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })), archives: vendorArchives.filter(({ id }) => !omitArchives.includes(id)), system_libraries: systemLibraries, - }; + }); + if (libraryOnly) { + matrix.flavors = Object.fromEntries(Object.entries(matrix.flavors).filter(([name]) => name.startsWith("library-"))); + matrix.archives = matrix.archives.filter(({ id }) => id === "libregexp" || id === "zlib"); + } + return matrix; } diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 7f03bfd85..970e6bd5a 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -84,9 +84,10 @@ async function build() { for (const [flavor, flavorSpec] of Object.entries(matrix.flavors)) { process.stdout.write(`building ${packageManifest.name} ${flavor} runtime\n`); const units = []; - for (const unit of matrix.runtime_units) { + for (const unit of flavorSpec.runtime_units ?? matrix.runtime_units) { const variants = []; - for (const variant of unit.variants) { + for (const baseVariant of unit.variants) { + const variant = { ...baseVariant, defines: [...(flavorSpec.defines ?? []), ...baseVariant.defines] }; const output = join(stagedOutputRoot, flavor, "runtime", variant.id, unit.source.replace(/\.c$/, ".o")); const includeFlags = [ ...(unit.source === "scr_regex.c" || variant.defines.includes("SCR_DYNAMIC") ? ["-I", quickjs] : []), @@ -121,7 +122,7 @@ async function build() { const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]]; licensed.push([join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"]); await Promise.all(licensed.map(async ([source, destination]) => { const output = join(buildRoot, destination); await mkdir(dirname(output), { recursive: true }); await copyFile(source, output); })); - const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; + const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; await writeFile(stagedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`); const suffix = `${process.pid}-${Math.random().toString(36).slice(2)}`; await installRuntimePack({ outputRoot, manifestPath, stagedOutputRoot, stagedManifestPath, backupRoot: join(packageRoot, `.runtime-pack-artifacts-backup-${suffix}`), backupManifestPath: join(packageRoot, `.runtime-pack-manifest-backup-${suffix}`) }); diff --git a/packages/runtime-pack-common/scripts/mobile-toolchain.mjs b/packages/runtime-pack-common/scripts/mobile-toolchain.mjs new file mode 100644 index 000000000..3cfebe2c7 --- /dev/null +++ b/packages/runtime-pack-common/scripts/mobile-toolchain.mjs @@ -0,0 +1,40 @@ +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +/** SDKs are packaging inputs. Library consumers only need the resulting objects. */ +export function mobileRuntimeConfig(name, env = process.env, platform = process.platform) { + const common = { + libraryOnly: true, + runtimeDefines: ["_GNU_SOURCE"], + threadArgs: ["-pthread"], + compileFlags: ["-ffunction-sections", "-fdata-sections"], + systemLibraries: [{ name: "m", predicate: true }], + }; + if (name === "ios-arm64" || name === "ios-simulator-arm64") { + if (platform !== "darwin") throw new Error("iOS runtime packs must be built with Xcode on macOS"); + const simulator = name === "ios-simulator-arm64"; + const sdk = simulator ? "iphonesimulator" : "iphoneos"; + const triple = `arm64-apple-ios15.0.0${simulator ? "-simulator" : ""}`; + const sysroot = execFileSync("xcrun", ["--sdk", sdk, "--show-sdk-path"], { encoding: "utf8", env }).trim(); + return { + ...common, platform: "darwin", compiler: "clang", archiver: "ar", compileFlags: [], + target: { name, llvm_triple: triple, architecture: "arm64", object_format: "macho", minimum_os: "15.0" }, + targetArgs: ["-target", triple, "-isysroot", sysroot], + systemLibraries: [{ name: "System", predicate: true }], + }; + } + if (name !== "android-arm64") throw new Error(`unsupported mobile runtime pack: ${name}`); + const ndk = env.ANDROID_NDK_ROOT ?? env.ANDROID_NDK_HOME; + if (!ndk) throw new Error("set ANDROID_NDK_ROOT to Android NDK 27 or newer to build the Android runtime pack"); + const host = platform === "darwin" ? "darwin-x86_64" : platform === "win32" ? "windows-x86_64" : "linux-x86_64"; + const toolchain = join(ndk, "toolchains", "llvm", "prebuilt", host); + const compiler = join(toolchain, "bin", platform === "win32" ? "clang.exe" : "clang"); + if (!existsSync(compiler)) throw new Error(`Android NDK compiler is missing: ${compiler}`); + return { + ...common, platform: "linux", compiler, + archiver: join(toolchain, "bin", platform === "win32" ? "llvm-ar.exe" : "llvm-ar"), + target: { name, llvm_triple: "aarch64-unknown-linux-android26", architecture: "arm64", object_format: "elf", minimum_os: "Android 26" }, + targetArgs: ["--target=aarch64-linux-android26", "--sysroot", join(toolchain, "sysroot")], + }; +} diff --git a/packages/runtime-pack-common/test/library-matrix.test.ts b/packages/runtime-pack-common/test/library-matrix.test.ts new file mode 100644 index 000000000..497b3d5e8 --- /dev/null +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "vitest"; +import { createRuntimePackMatrix } from "../runtime-pack-matrix.mjs"; + +const matrix = createRuntimePackMatrix({ + target: { object_format: "macho" }, compileFlags: [], systemLibraries: [], +}); + +test("library pack modes preserve pure runtime features and exclude ambient event-loop code", () => { + for (const optimization of ["release", "dev"]) { + for (const mode of ["library", "library-thread"]) { + const flavor = matrix.flavors[`${mode}-${optimization}`]; + expect(flavor.defines).toEqual(mode === "library" ? ["SCR_LIB"] : ["SCR_LIB", "SCR_THREAD_INSTANCES"]); + expect(flavor.optimization).toBe(optimization === "release" ? "-O2" : "-O0"); + const sources = flavor.runtime_units.map((unit) => unit.source); + expect(sources).toContain("scr_library.c"); + for (const source of ["scr_async.c", "scr_crypto_async.c", "scr_child.c", "scr_ffi.c", "scr_island.c", "scr_net.c"]) expect(sources).not.toContain(source); + for (const source of ["scr_regex.c", "scr_assert.c", "scr_events_emitter.c", "scr_zlib.c", "scr_inspect.c", "scr_copying.c"]) expect(sources).toContain(source); + for (const unit of flavor.runtime_units) for (const variant of unit.variants) expect(variant.defines).not.toContain("SCR_DYNAMIC"); + expect(flavor.runtime_units.find((unit) => unit.source === "scr_bytes.c").variants.map((variant) => variant.id)).toEqual(["default", "text-decoder-legacy"]); + } + } + expect(matrix.runtime_units.some((unit) => unit.source === "scr_library.c")).toBe(false); +}); + +test("mobile packs ship only library modes and their permitted vendor archives", () => { + const mobile = createRuntimePackMatrix({ + target: { object_format: "elf" }, compileFlags: [], systemLibraries: [], libraryOnly: true, + }); + expect(Object.keys(mobile.flavors).sort()).toEqual([ + "library-dev", "library-release", "library-thread-dev", "library-thread-release", + ]); + expect(mobile.archives.map(({ id }) => id)).toEqual(["libregexp", "zlib"]); +}); diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index b8ea3af4d..b661ae682 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -9,12 +9,6 @@ "url": "git+https://github.com/vercel-labs/scriptc.git", "directory": "packages/runtime-win32-x64-msvc" }, - "os": [ - "win32" - ], - "cpu": [ - "x64" - ], "files": [ "artifacts", "runtime-pack.json" diff --git a/packages/runtime-win32-x64-msvc/scripts/build.mjs b/packages/runtime-win32-x64-msvc/scripts/build.mjs index c41d6fb6c..fbe45d84f 100644 --- a/packages/runtime-win32-x64-msvc/scripts/build.mjs +++ b/packages/runtime-win32-x64-msvc/scripts/build.mjs @@ -1,5 +1,5 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; -if (process.platform !== "win32" || process.arch !== "x64") { process.stdout.write("@scriptc/runtime-win32-x64-msvc: skipped on this host\n"); process.exit(0); } +if (process.env.SCRIPTC_BUILD_CROSS !== "1" && (process.platform !== "win32" || process.arch !== "x64")) { process.stdout.write("@scriptc/runtime-win32-x64-msvc: skipped on this host\n"); process.exit(0); } process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); // The helper emits x64 COFF objects, whose calling convention is compatible // with Zig's MinGW runtime. The runtime itself intentionally uses the diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 1a980e8d5..068a3f7f7 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -18,11 +18,11 @@ * skips only when it already FIRED — mid-emit removal of a not-yet-run * once listener does not skip it, matching the wrapper's `fired` check. * - * Literal-name dispatch is C-variadic: the emit call site passes the event tuple as - * typed C values, and each listener invokes through a compiler-EMITTED - * adapter `void inv(ScrClosure *cb, va_list ap)` that va_args exactly the - * listener's own parameter prefix, retains the +1 the callee owns per the - * universal convention, and calls cb->fn. The frontend's per-event tuple + * Literal-name dispatch is C-variadic: LLVM emit sites pass borrowed + * pointers, with scalars stored in call-lived stack slots. A runtime shim + * reads the listener's parameter prefix and hands it to the emitted + * adapter, which loads scalars, retains the references the callee owns, + * and calls the original closure. The frontend's per-event tuple * unification is what makes every adapter's reads agree with every emit * site's writes. Internal (meta-event) emits reuse the same variadic * entry, passing the event NAME as the one argument — meta listeners are @@ -441,6 +441,17 @@ void scr_ee_inv_fixed4(ScrClosure *cb, va_list ap) { ((void (*)(ScrClosure *, void *, void *, void *, void *))cb->fn)(cb, a0, a1, a2, a3); } +void scr_ee_inv_args(ScrClosure *cb, va_list ap) { + va_list cursor; + va_copy(cursor, ap); + ((void (*)(ScrClosure *, void *))cb->fn)(cb, &cursor); + va_end(cursor); +} + +void *scr_ee_arg_next(void *cursor) { + return va_arg(*(va_list *)cursor, void *); +} + /* Removes one entry (by index) from a bucket's live list and fires the * 'removeListener' meta event AFTER the removal, Node's order. Drops the * bucket when emptied — except in shape mode, where the emptied name diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index ab120d133..42a0d19a0 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -1738,14 +1738,18 @@ ScrEmitter *scr_emitter_on_via(ScrEmitter *em, ScrStr *name, ScrClosure *orig /* * typed stack slots and reference values ride directly. The runtime's own * emits (data/error/pipe/meta) carry pointers only, so the shims read every * tuple either lane produces on 32- and 64-bit targets. - * SCR_EE_FIXED_MAX is the registry's audited arity ceiling — backends - * refuse listeners past it rather than guess. */ + * The fixed shims cover the common short tuples. Longer adapters use the + * opaque cursor interface below without depending on the va_list ABI. */ #define SCR_EE_FIXED_MAX 4 void scr_ee_inv_fixed0(ScrClosure *cb, va_list ap); void scr_ee_inv_fixed1(ScrClosure *cb, va_list ap); void scr_ee_inv_fixed2(ScrClosure *cb, va_list ap); void scr_ee_inv_fixed3(ScrClosure *cb, va_list ap); void scr_ee_inv_fixed4(ScrClosure *cb, va_list ap); +/* Invoke cb->fn(cb, cursor). The cursor borrows a private va_list copy for + * this call; each next operation reads one borrowed pointer argument. */ +void scr_ee_inv_args(ScrClosure *cb, va_list ap); +void *scr_ee_arg_next(void *cursor); /* removeListener/off (LAST matching occurrence, Node's search order) and * removeAllListeners; both return em +1. 'removeListener' fires after * each removal when listened for. */ diff --git a/packages/runtime/src/scr_win.c b/packages/runtime/src/scr_win.c index 64bc3b838..ecf1221dc 100644 --- a/packages/runtime/src/scr_win.c +++ b/packages/runtime/src/scr_win.c @@ -16,6 +16,11 @@ #include #include +/* LLVM's MSVC-target objects reference this marker when they use floating + * point. MinGW supplies the arithmetic runtime but not the MSVC marker. + * Select-any allows an embedding application's CRT to provide it as well. */ +__declspec(selectany) int _fltused = 0x9875; + #ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 #endif diff --git a/packages/runtime/test/runtime.test.ts b/packages/runtime/test/runtime.test.ts index c497a4cb9..f88a71ba8 100644 --- a/packages/runtime/test/runtime.test.ts +++ b/packages/runtime/test/runtime.test.ts @@ -13,7 +13,7 @@ const RUNTIME_SOURCES = ["scr_number.c", "scr_bigint.c", "scr_string.c", "scr_ar ); // Full runtime smoke under ASan + the RC audit: proves the API works and the -// hand-modeled ownership discipline (the same one emit-c.ts generates) is +// hand-modeled ownership discipline (the same one the LLVM emitter uses) is // leak- and double-free-clean. test("runtime smoke.c: output exact, ASan and RC audit clean", async () => { const buildDir = join(testDir, "build"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b1f1746c..9b3ddb48d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,12 +80,21 @@ importers: '@scriptc/llvm-win32-x64-msvc': specifier: workspace:* version: link:../llvm-win32-x64-msvc + '@scriptc/runtime-android-arm64': + specifier: workspace:* + version: link:../runtime-android-arm64 '@scriptc/runtime-darwin-arm64': specifier: workspace:* version: link:../runtime-darwin-arm64 '@scriptc/runtime-darwin-x64': specifier: workspace:* version: link:../runtime-darwin-x64 + '@scriptc/runtime-ios-arm64': + specifier: workspace:* + version: link:../runtime-ios-arm64 + '@scriptc/runtime-ios-simulator-arm64': + specifier: workspace:* + version: link:../runtime-ios-simulator-arm64 '@scriptc/runtime-linux-arm64-gnu': specifier: workspace:* version: link:../runtime-linux-arm64-gnu @@ -121,10 +130,16 @@ importers: packages/runtime: {} + packages/runtime-android-arm64: {} + packages/runtime-darwin-arm64: {} packages/runtime-darwin-x64: {} + packages/runtime-ios-arm64: {} + + packages/runtime-ios-simulator-arm64: {} + packages/runtime-linux-arm64-gnu: {} packages/runtime-linux-arm64-musl: {} diff --git a/scripts/build-native-compiler.mts b/scripts/build-native-compiler.mts index 353cf17fc..2337f9530 100644 --- a/scripts/build-native-compiler.mts +++ b/scripts/build-native-compiler.mts @@ -31,7 +31,7 @@ writeFileSync(ffi, JSON.stringify({ ...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), libraries: [object], }, null, 2) + "\n"); const result = await compile(join(root, "packages/compiler/src/native/main.ts"), { - outDir: output, outPath: executable, backend: "c", optimization: "dev", dynamic: false, ffiProfilePath: ffi, sanitize, + outDir: output, outPath: executable, backend: "llvm", optimization: "release", strip: true, dynamic: false, ffiProfilePath: ffi, sanitize, }); if (!result.ok) throw new Error(result.diagnostics.map((item) => `${item.loc ? `${item.loc.file}:${item.loc.start}: ` : ""}${item.code}: ${item.message}`, @@ -46,8 +46,6 @@ const manifest: NativeToolchainManifest = { compiler_version: (JSON.parse(readFileSync(join(root, "packages/compiler/package.json"), "utf8")) as { version: string }).version, target: target.name, ts7: relative(relativeRoot, ts7Executable()), llvm_package: packagePath(target.helper.packageName), runtime_pack: packagePath(target.runtimePackPackage), - runtime_headers: relative(relativeRoot, join(root, "packages/runtime/src")), - c_compiler: compiler, c_compiler_args: compilerArgs, linker: process.env["SCRIPTC_LINKER"] ?? target.defaultLinker, linker_args: process.env["SCRIPTC_LINKER"] === undefined ? [...target.defaultLinkerArgs] : [], dsymutil: process.env["SCRIPTC_DSYMUTIL"] ?? "dsymutil", diff --git a/scripts/sandbox-config.mjs b/scripts/sandbox-config.mjs index 7921659cc..fffb8bf78 100644 --- a/scripts/sandbox-config.mjs +++ b/scripts/sandbox-config.mjs @@ -238,3 +238,27 @@ export function requiredSandboxImageConfig(env) { } return config; } + +/** A prepared snapshot reuses the same pinned tools as a custom image. + * The runner still replaces the source tree and rebuilds native artifacts. */ +export function sandboxTestSourceConfig(env) { + loadLocalEnv(); + const source = env ?? process.env; + const snapshot = source.SCRIPTC_SANDBOX_SNAPSHOT?.trim(); + if (snapshot) { + if (source.SCRIPTC_SANDBOX_IMAGE?.trim()) { + throw new Error("Set only one of SCRIPTC_SANDBOX_SNAPSHOT and SCRIPTC_SANDBOX_IMAGE"); + } + if (!/^snap_[A-Za-z0-9]+$/.test(snapshot)) { + throw new Error("SCRIPTC_SANDBOX_SNAPSHOT must be a Sandbox snapshot ID (snap_...)"); + } + return { prepared: true, reference: snapshot, description: "prepared snapshot", createArgs: ["--snapshot", snapshot] }; + } + const image = sandboxImageConfig(source); + return { + prepared: image.custom, + reference: image.sandboxImage, + description: image.custom ? "custom VCR image" : "managed fallback image", + createArgs: ["--image", image.sandboxImage], + }; +} diff --git a/scripts/sandbox-test.mjs b/scripts/sandbox-test.mjs index e4ed08c94..67b0508ef 100644 --- a/scripts/sandbox-test.mjs +++ b/scripts/sandbox-test.mjs @@ -9,7 +9,7 @@ import { parseArgs } from "node:util"; import { fileURLToPath } from "node:url"; import { sandboxBootstrapCommand, - sandboxImageConfig, + sandboxTestSourceConfig, sandboxRunnerConfig, sandboxTestWorkerAllocation, sandboxVercelConfig, @@ -61,7 +61,7 @@ const invariantRemoteFiles = [ "packages/cli/test/paths.test.ts", "packages/compiler/src/library/int-infer.test.ts", "packages/compiler/test/cjs-lexer.test.ts", - "packages/compiler/test/emit-c.test.ts", + "packages/compiler/test/emit-llvm.test.ts", "packages/compiler/test/ir.test.ts", "packages/compiler/test/llvm-runtime-abi.test.ts", "packages/compiler/test/ts7/bench.test.ts", @@ -187,6 +187,7 @@ Environment: VERCEL_OIDC_TOKEN Preferred project-scoped Sandbox credential VERCEL_TOKEN Access-token fallback; also set VERCEL_TEAM_ID + VERCEL_PROJECT_ID SCRIPTC_SANDBOX_IMAGE Optional fully qualified VCR image (default: vercel/sandbox/universal) + SCRIPTC_SANDBOX_SNAPSHOT Optional prepared snapshot with the pinned toolchain (exclusive with IMAGE) SCRIPTC_SANDBOX_VCPUS vCPUs per sandbox (default: 8) SCRIPTC_SANDBOX_TIMEOUT sandbox and command timeout (default: 45m) SCRIPTC_TEST_WORKERS Vitest workers per sandbox (default: 4) @@ -195,9 +196,8 @@ Environment: process.exit(0); } -const imageConfig = sandboxImageConfig(); -const { sandboxImage: image } = imageConfig; -const bootstrapCommand = sandboxBootstrapCommand(imageConfig.custom); +const sourceConfig = sandboxTestSourceConfig(); +const bootstrapCommand = sandboxBootstrapCommand(sourceConfig.prepared); const vercelConfig = sandboxVercelConfig(); const vercelProcessEnv = sandboxVercelEnvironment(vercelConfig); const { @@ -451,11 +451,10 @@ const execIn = async ( }; async function preflight() { - const customImage = imageConfig.custom ? "custom VCR image" : "managed fallback image"; console.log("Sandbox preflight:"); console.log(` auth: ${vercelConfig.authSource}`); console.log(` scope: ${vercelConfig.scopeSource}`); - console.log(` image: ${image} (${customImage})`); + console.log(` source: ${sourceConfig.reference} (${sourceConfig.description})`); console.log(` shape: ${workers.length} sandboxes, ${vcpus} vCPUs each`); try { @@ -530,8 +529,7 @@ async function createWorker(worker) { "create", "--name", worker.name, - "--image", - image, + ...sourceConfig.createArgs, "--timeout", sandboxTimeout, "--vcpus", @@ -665,7 +663,7 @@ let failure; try { await preflight(); console.log( - `Running ${lanes.join("+")} corpus lanes in ${workers.length} ${vcpus}-vCPU sandboxes from ${image} (${shardCount} shards/lane).`, + `Running ${lanes.join("+")} corpus lanes in ${workers.length} ${vcpus}-vCPU sandboxes from ${sourceConfig.reference} (${shardCount} shards/lane).`, ); console.log("Packing the exact tracked + untracked, non-ignored worktree..."); await createArchive(archive); @@ -723,7 +721,9 @@ try { ); } await execIn(worker, "pnpm", ["install", "--frozen-lockfile"], {}, "", 2 * 60_000); - await execIn(worker, "pnpm", ["build"], {}, "", 2 * 60_000); + // Resetting the source tree removes dist but preserves node_modules, + // including TypeScript's incremental metadata. Rebuild both together. + await execIn(worker, "pnpm", ["build:fresh"], {}, "", 2 * 60_000); // Workspace builds deliberately do not rebuild packaged native artifacts. // Every remote lane needs the Linux helper and runtime from this worktree. await execIn(worker, "pnpm", ["--filter", "@scriptc/llvm-linux-x64-gnu", "build:native"], {}, "LLVM helper", 5 * 60_000); @@ -749,7 +749,7 @@ try { "runtime toolchain cleanup", 60_000, ); - }, imageConfig.custom ? workers.length : 8); + }, sourceConfig.prepared ? workers.length : 8); await allWorkers("Testing", async (worker) => { const sharedTestEnv = { diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs index 5c5b18e07..f6b27c7ac 100644 --- a/scripts/sync-versions.mjs +++ b/scripts/sync-versions.mjs @@ -22,6 +22,7 @@ for (const pkg of [ "runtime-linux-x64-gnu", "runtime-linux-arm64-gnu", "runtime-linux-x64-musl", "runtime-linux-arm64-musl", "runtime-win32-x64-msvc", "runtime-wasm32-wasi", + "runtime-ios-arm64", "runtime-ios-simulator-arm64", "runtime-android-arm64", "compiler", "llvm-darwin-arm64", "llvm-darwin-x64", "llvm-linux-x64-gnu", "llvm-linux-arm64-gnu", diff --git a/tests/corpus/2448-entries-null-arms-regex-arrays.ts b/tests/corpus/2448-entries-null-arms-regex-arrays.ts index 240483f61..c03260006 100644 --- a/tests/corpus/2448-entries-null-arms-regex-arrays.ts +++ b/tests/corpus/2448-entries-null-arms-regex-arrays.ts @@ -1,7 +1,7 @@ // Three static families in one program: Object.entries/values over // unit-armed (null | undefined) fields, RegExp values as array elements, // and JSON.stringify over a dyn root holding undefined — all byte-exact -// against Node on both backends. +// against Node through LLVM. // // ── Object.entries/values over shapes whose fields are `null | undefined` — // the mixed-defaults spread idiom (`{ ...defaults, ...overrides }` where a @@ -88,7 +88,7 @@ console.log(popped === undefined ? "none" : popped.source, grown.length); // ── JSON.stringify over a dyn root holding undefined ───────────────────── // JSON.stringify(undefined) is the undefined VALUE; printing it spells the -// word — both backends ride the same dyn walker (a nested-position writer +// word — the dyn walker handles both paths (a nested-position writer // would spell null; the root is special). const u: unknown = undefined; console.log(JSON.stringify(u)); diff --git a/tests/corpus/2566-promise-reject-dyn-reason.js b/tests/corpus/2566-promise-reject-dyn-reason.js index be9200a91..41091b6e9 100644 --- a/tests/corpus/2566-promise-reject-dyn-reason.js +++ b/tests/corpus/2566-promise-reject-dyn-reason.js @@ -1,11 +1,10 @@ // Promise.reject with a CHECKED-DYNAMIC reason (`Promise.reject(value)` // where value rode an untyped JS param — the tracingChannel promise -// suite's shape): the dyn value IS the rejection payload on both -// backends. Every reason kind rides the thrown-dyn representation into +// suite's shape): the dyn value is the rejection payload. Every reason kind rides the thrown-dyn representation into // catch/await observers, settled-state choreography intact (the .catch // callbacks run as microtasks after the synchronous tail). The LLVM tier // used to crash the CLI on the non-object-kinded reason type; now it -// emits the same scr_throw_ref shape the C backend always had. +// emits the scr_throw_ref representation for the dynamic value. // Node is the oracle byte-for-byte. "use strict"; diff --git a/tests/corpus/2569-upcast-identity.ts b/tests/corpus/2569-upcast-identity.ts index 40d6e9e91..6f3456c92 100644 --- a/tests/corpus/2569-upcast-identity.ts +++ b/tests/corpus/2569-upcast-identity.ts @@ -1,7 +1,7 @@ // Identity compares across hierarchy upcasts: one object retained and // released through BOTH its base-typed and derived-typed spellings in a // single statement frame, plus a freshly constructed operand in the same -// call — the emitted C type-puns the object header through both struct +// call — the emitted LLVM type-puns the object header through both struct // types, which clang's TBAA at -O2 used to miscompile into a premature // free (the global still owned the object when release_globals ran). class Base { diff --git a/tests/corpus/2678-large-string-sparse-index.ts b/tests/corpus/2678-large-string-sparse-index.ts index 2fd509587..eb89526b3 100644 --- a/tests/corpus/2678-large-string-sparse-index.ts +++ b/tests/corpus/2678-large-string-sparse-index.ts @@ -1,6 +1,6 @@ // Large mixed-Unicode indexed-string regression. The output is compact and // deterministic; the runtime white-box test owns the complexity bound while -// this corpus pins C/LLVM semantics against Node. +// this corpus pins native semantics against Node. const piece = "aé😀é"; // UTF-16 units: a, é, high/low 😀, e, combining mark const text = piece.repeat(12000); const positions = [0, 1, 2, 3, 4, 5, 6, 31111, 52799, text.length - 6]; diff --git a/tests/corpus/2868-empty-record-promise-presence.js b/tests/corpus/2868-empty-record-promise-presence.js index 1a58de7f4..396a09ee8 100644 --- a/tests/corpus/2868-empty-record-promise-presence.js +++ b/tests/corpus/2868-empty-record-promise-presence.js @@ -17,3 +17,10 @@ function isThenable(promise) { } console.log(isThenable(undefined), isThenable(Promise.resolve("ready"))); + +/** @param {Promise | undefined} promise */ +function optionalThenable(promise) { + if (promise?.then && typeof promise.then === "function") return true; + return false; +} +console.log(optionalThenable(undefined), optionalThenable(Promise.resolve("ready"))); diff --git a/tests/corpus/2991-array-indexed-equality.ts b/tests/corpus/2991-array-indexed-equality.ts index 28e492543..daec918e2 100644 --- a/tests/corpus/2991-array-indexed-equality.ts +++ b/tests/corpus/2991-array-indexed-equality.ts @@ -37,7 +37,7 @@ function fail(): number { trace += "!"; throw new Error("index"); } try { console.log(receiver("A")[index("a")] === receiver("B")[fail()]); } catch (error) { console.log(error instanceof Error, trace); } // Repeated conditions exercise hidden-local cleanup on continue/break and -// exceptions; both backends run under the RC audit as well as plain mode. +// exceptions; LLVM runs under the RC audit as well as plain mode. let count = 0; for (let i = 0; i < 500; i++) { const x = ["x".repeat(i % 17 + 1)]; diff --git a/tests/corpus/2992-stream-rows.ts b/tests/corpus/2992-stream-rows.ts index 7a4572250..ec215c868 100644 --- a/tests/corpus/2992-stream-rows.ts +++ b/tests/corpus/2992-stream-rows.ts @@ -1,5 +1,5 @@ // Redirected stdio has no geometry. PTY reads and resize are covered by the -// terminal-geometry harness; both backends must also preserve undefined here. +// terminal-geometry harness; LLVM must also preserve undefined here. const out = (process.stdout as typeof process.stdout & { rows?: number }).rows; const err = (process.stderr as typeof process.stderr & { rows?: number }).rows; console.log(out === undefined, err === undefined, out ?? 24, err ?? 48); diff --git a/tests/corpus/4030-runtime-object-truthiness.ts b/tests/corpus/4030-runtime-object-truthiness.ts new file mode 100644 index 000000000..3ffa34759 --- /dev/null +++ b/tests/corpus/4030-runtime-object-truthiness.ts @@ -0,0 +1,10 @@ +import { createServer, Server } from "node:net"; + +function serverPresent(server: Server | null): boolean { + return !!server; +} + +const server = createServer(() => {}); +console.log("streams", !!process.stdin, !!process.stdout, !!process.stderr); +console.log("input", process.stdin ? "present" : "absent"); +console.log("server", serverPresent(server), serverPresent(null), !!server); diff --git a/tests/corpus/4031-event-emitter-long-tuples.ts b/tests/corpus/4031-event-emitter-long-tuples.ts new file mode 100644 index 000000000..bc361665d --- /dev/null +++ b/tests/corpus/4031-event-emitter-long-tuples.ts @@ -0,0 +1,36 @@ +import { EventEmitter } from "node:events"; + +const emitter = new EventEmitter(); +const box = { label: "record" }; +const bytes = Buffer.from("buffer"); +const values = [2, 3]; +let nested = false; +function listener(a: number, b: string, c: boolean, d: { label: string }, e: Buffer, f: number[], g: number): string { + console.log("long", a, b, c, d.label, e.toString(), f.join(":"), g); + if (!nested) { + nested = true; + emitter.emit("tuple", 8, "inner", false, box, bytes, values, 9); + } + return b + d.label; +} +emitter.on("tuple", listener); +emitter.once("tuple", (a: number, b: string, c: boolean, d: { label: string }, e: Buffer) => { + console.log("prefix", a, b, c, d.label, e.toString()); +}); +emitter.on("tuple", () => { console.log("empty"); }); +console.log("identity", emitter.listenerCount("tuple", listener)); +console.log("emit", emitter.emit("tuple", 1, "outer", true, box, bytes, values, 7)); +emitter.off("tuple", listener); +console.log("remaining", emitter.listenerCount("tuple")); +emitter.emit("tuple", 10, "last", true, box, bytes, values, 11); + +emitter.on("throwing", (a: number, b: string, c: boolean, d: Buffer, e: number[]) => { + console.log("throw", a, b, c, d.toString(), e.length); + throw new Error("listener failed"); +}); +try { + emitter.emit("throwing", 1, "error", true, bytes, values); +} catch (error) { + console.log((error as Error).message); +} +console.log(box.label, bytes.toString(), values.join(":")); diff --git a/tests/corpus/420-dead-strip-modules/main.ts b/tests/corpus/420-dead-strip-modules/main.ts index b04a3ec1c..1a178c3e1 100644 --- a/tests/corpus/420-dead-strip-modules/main.ts +++ b/tests/corpus/420-dead-strip-modules/main.ts @@ -1,7 +1,7 @@ // Dead-strip across modules: only what the entry reaches is compiled. // The unused exports in lib.ts hold constructs the static compiler // rejects — the build succeeds anyway, and the harness dead-strip test -// greps the emitted C to pin that they leave no trace. +// greps the emitted LLVM to pin that they leave no trace. import "./side.ts"; import { Gadget, double, pick, used } from "./lib.ts"; diff --git a/tests/corpus/llvm-read-receiver-lifetime.ts b/tests/corpus/llvm-read-receiver-lifetime.ts new file mode 100644 index 000000000..607c4ec88 --- /dev/null +++ b/tests/corpus/llvm-read-receiver-lifetime.ts @@ -0,0 +1,88 @@ +type Leaf = { text: string }; +type Branch = { kind: "branch"; child: Leaf } | { kind: "empty"; reason: string }; +type Root = { branch: Branch }; + +function root(text: string): Root { + return { branch: { kind: "branch", child: { text } } }; +} + +let current = root("first"); +function replace(): string { + current = root("second"); + return "!"; +} +if (current.branch.kind === "branch") { + console.log(current.branch.child.text + replace()); +} +console.log(current.branch.kind); + +function replaceChild(value: Leaf): string { + value.text = "changed"; + return "?"; +} +if (current.branch.kind === "branch") { + console.log(current.branch.child.text + replaceChild(current.branch.child)); + console.log(current.branch.child.text); +} + +let calls = 0; +function temporary(): Root { + calls++; + return root("temporary"); +} +function project(value: Root): Leaf { + if (value.branch.kind === "branch") return value.branch.child; + throw new Error(value.branch.reason); +} +const saved = project(temporary()); +console.log(saved.text, calls); +console.log(temporary().branch.kind, calls); + +class Holder { + nested: Root; + constructor(nested: Root) { this.nested = nested; } +} +function fromClass(holder: Holder): string { + const branch = holder.nested.branch; + return branch.kind === "branch" ? branch.child.text : branch.reason; +} +console.log(fromClass(new Holder(root("class")))); +console.log(fromClass(new Holder({ branch: { kind: "empty", reason: "none" } }))); + +function captured(): void { + let value = root("captured"); + const update = (): string => { value = root("updated"); return "."; }; + if (value.branch.kind === "branch") console.log(value.branch.child.text + update()); + if (value.branch.kind === "branch") console.log(value.branch.child.text); +} +captured(); + +function fail(): string { throw new Error("failed"); } +try { + console.log(project(temporary()).text + fail()); +} catch (error) { + if (error instanceof Error) console.log(error.message, calls); +} + +for (let i = 0; i < 500; i++) { + const value = project(root("loop" + i)); + if (i === 499) console.log(value.text); +} + +type Shared = + | { kind: "one"; text: string; enabled: boolean; count: number } + | { kind: "two"; text: string; enabled: boolean; extra: string }; +function shared(value: Shared): void { + console.log(value.kind, value.text, value.enabled); +} +shared({ kind: "one", text: "first", enabled: true, count: 1 }); +shared({ kind: "two", text: "second", enabled: false, extra: "tail" }); + +type Different = + | { kind: "small"; padding: boolean; text: string } + | { kind: "wide"; padding: number; text: string } + | { text: string; kind: "reordered" }; +function different(value: Different): void { console.log(value.text, value.kind); } +different({ kind: "small", padding: true, text: "boolean prefix" }); +different({ kind: "wide", padding: 2, text: "number prefix" }); +different({ text: "different offset", kind: "reordered" }); diff --git a/tests/corpus/logical-runtime-optional-record.ts b/tests/corpus/logical-runtime-optional-record.ts new file mode 100644 index 000000000..3bbba93b3 --- /dev/null +++ b/tests/corpus/logical-runtime-optional-record.ts @@ -0,0 +1,66 @@ +type ValueType = { kind: "number" } | { kind: "object"; name: string }; +type Expression = + | { kind: "number"; type: ValueType; value: number } + | { kind: "binary"; type: ValueType; left: Expression; right: Expression } + | { kind: "call"; type: ValueType; args: Expression[] }; + +let predicateCalls = 0; +function isObject(type: ValueType): boolean { + predicateCalls++; + return type.kind === "object"; +} + +let tailCalls = 0; +function tail(): string { + tailCalls++; + return "ready"; +} + +function check(expression: Expression): void { + if (expression.kind !== "call") return; + const first = expression.args[0]; + const valid = first && isObject(first.type); + const chained = first && isObject(first.type) && tail(); + console.log(valid, chained, predicateCalls, tailCalls); +} + +let reads = 0; +function read(values: Expression[]): Expression { + reads++; + return values[0]; +} + +function evaluateOnce(values: Expression[]): void { + const value = read(values) && tail(); + console.log(value, reads, tailCalls); +} + +const number: Expression = { kind: "number", type: { kind: "number" }, value: 1 }; +const binary: Expression = { kind: "binary", type: { kind: "number" }, left: number, right: number }; +const object: Expression = { kind: "number", type: { kind: "object", name: "sample" }, value: 2 }; +check({ kind: "call", type: { kind: "number" }, args: [] }); +check({ kind: "call", type: { kind: "number" }, args: [binary] }); +check({ kind: "call", type: { kind: "number" }, args: [object] }); +evaluateOnce([]); +evaluateOnce([binary]); + +function scalarChoices(values: boolean[]): void { + const both = values[0] && values[1]; + const either = values[0] || values[1]; + const fallback = values[0] || false; + console.log(both, either, fallback); +} +scalarChoices([]); +scalarChoices([false]); +scalarChoices([true]); +scalarChoices([false, true]); +scalarChoices([true, false]); + +function explicitUnits(value: Expression | null | undefined): void { + const result = value && isObject(value.type); + console.log(result); +} +explicitUnits(null); +explicitUnits(undefined); +explicitUnits(binary); +explicitUnits(object); diff --git a/tests/corpus/nullish-long-chain.ts b/tests/corpus/nullish-long-chain.ts index 54d0a7818..6e3d0b8f4 100644 --- a/tests/corpus/nullish-long-chain.ts +++ b/tests/corpus/nullish-long-chain.ts @@ -49,3 +49,57 @@ console.log(sequence); sequence = ""; console.log((text("a", undefined) ?? text("b", null)) ?? text("c", "end") ?? text("d", "unused")); console.log(sequence); + +// Logical chains also occur in compiler predicates and retain operand values. +function logicalDispatch(winner: number): void { + const calls: number[] = []; + function probe(index: number): number { + calls.push(index); + return index === winner ? index + 1 : 0; + } + const found = + probe(0) || probe(1) || probe(2) || probe(3) || + probe(4) || probe(5) || probe(6) || probe(7) || + probe(8) || probe(9) || probe(10) || probe(11) || + probe(12) || probe(13) || probe(14) || probe(15) || + probe(16) || probe(17) || probe(18) || probe(19) || + probe(20) || probe(21) || probe(22) || probe(23) || + probe(24) || probe(25) || probe(26) || probe(27) || + probe(28) || probe(29) || probe(30) || probe(31) || + probe(32) || probe(33) || probe(34) || probe(35) || + probe(36) || probe(37) || probe(38) || probe(39) || + probe(40) || probe(41) || probe(42) || probe(43) || + probe(44) || probe(45) || probe(46) || probe(47) || + probe(48) || probe(49) || probe(50) || probe(51) || + probe(52) || probe(53) || probe(54) || probe(55) || + probe(56) || probe(57) || probe(58) || probe(59) || + probe(60) || probe(61) || probe(62) || probe(63); + console.log("or", winner, found, calls.join(",")); + calls.length = 0; + function accept(index: number): number { + calls.push(index); + return index === winner ? 0 : index + 1; + } + const accepted = + accept(0) && accept(1) && accept(2) && accept(3) && + accept(4) && accept(5) && accept(6) && accept(7) && + accept(8) && accept(9) && accept(10) && accept(11) && + accept(12) && accept(13) && accept(14) && accept(15) && + accept(16) && accept(17) && accept(18) && accept(19) && + accept(20) && accept(21) && accept(22) && accept(23) && + accept(24) && accept(25) && accept(26) && accept(27) && + accept(28) && accept(29) && accept(30) && accept(31) && + accept(32) && accept(33) && accept(34) && accept(35) && + accept(36) && accept(37) && accept(38) && accept(39) && + accept(40) && accept(41) && accept(42) && accept(43) && + accept(44) && accept(45) && accept(46) && accept(47) && + accept(48) && accept(49) && accept(50) && accept(51) && + accept(52) && accept(53) && accept(54) && accept(55) && + accept(56) && accept(57) && accept(58) && accept(59) && + accept(60) && accept(61) && accept(62) && accept(63); + console.log("and", winner, accepted, calls.join(",")); +} +logicalDispatch(0); +logicalDispatch(31); +logicalDispatch(63); +logicalDispatch(64); diff --git a/tests/corpus/stdio-write-errors.mjs b/tests/corpus/stdio-write-errors.mjs index aa4fc4766..87ce0ef53 100644 --- a/tests/corpus/stdio-write-errors.mjs +++ b/tests/corpus/stdio-write-errors.mjs @@ -1,6 +1,11 @@ import { closeSync } from 'node:fs'; const output = process.stdout; const error = process.stderr; +function announce() { + output.write('ready\n'); + error.write('starting\n'); +} +announce(); function removed() { error.write('removed\n'); } output.on('error', removed); output.off('error', removed); diff --git a/tests/corpus/union-nested-layout-discriminant.ts b/tests/corpus/union-nested-layout-discriminant.ts new file mode 100644 index 000000000..37c03ef4c --- /dev/null +++ b/tests/corpus/union-nested-layout-discriminant.ts @@ -0,0 +1,61 @@ +// Mapping a recursive variant before the whole union preserves separate +// layout ids for bin and logical. Later inferred literals can share either. +type Expr = + | { kind: "number"; value: number } + | { kind: "bin"; op: "+" | "==="; left: Expr; right: Expr } + | { kind: "logical"; op: "&&" | "||"; left: Expr; right: Expr } + | { kind: "nullish"; left: Expr; right: Expr } + | { kind: "ternary"; cond: Expr; then: Expr; else_: Expr }; + +function binary(value: Expr & { kind: "bin" }): Expr { return value; } + +function inferred(left: Expr, right: Expr): Expr { + const value = { kind: "bin" as const, op: "+" as const, left, right }; + return value; +} + +function callback(left: Expr, right: Expr): () => Expr { + return () => ({ kind: "bin" as const, op: "+" as const, left, right }); +} + +function build(left: Expr, right: Expr, conditional: boolean): Expr { + const missing = conditional ? { + kind: "ternary" as const, + cond: { kind: "bin" as const, op: "===" as const, left, right }, + then: left, + else_: right, + } : left; + return missing; +} + +function inspect(root: Expr): void { + const pending = [root]; + while (pending.length !== 0) { + const e = pending.pop()!; + if (e.kind === "logical" || e.kind === "nullish") { + console.log(e.kind); + pending.push(e.right, e.left); + } else if (e.kind === "ternary") { + pending.push(e.else_, e.then, e.cond); + } else { + console.log(e.kind); + } + } +} +const left: Expr = { kind: "number", value: 1 }; +const right: Expr = { kind: "number", value: 2 }; +console.log(binary({ kind: "bin", op: "+", left, right }).kind); +inspect(build(left, right, true)); +inspect(build(left, right, false)); +inspect(inferred(left, right)); +inspect(callback(left, right)()); +inspect({ kind: "logical", op: "&&", left, right }); +inspect({ kind: "nullish", left, right }); + +// Native compilation of array search synthesizes a branching expression +// with a nested binary comparison, exercising this conversion in the compiler. +function packageSegment(path: string): number { + return path.split("/").lastIndexOf("node_modules"); +} +console.log(packageSegment("/workspace/node_modules/package")); +console.log(packageSegment("/workspace/src")); diff --git a/tests/fixtures/self-hosting/backend-analysis.ts b/tests/fixtures/self-hosting/backend-analysis.ts index 86a5453ba..866ec0a79 100644 --- a/tests/fixtures/self-hosting/backend-analysis.ts +++ b/tests/fixtures/self-hosting/backend-analysis.ts @@ -5,7 +5,7 @@ import { scalarizeNumericRecords } from "../../../packages/compiler/src/ir/scala import { findConstantNumericTables } from "../../../packages/compiler/src/ir/constant-tables.js"; import { matchIntegerBytesForLoop } from "../../../packages/compiler/src/ir/integer-loops.js"; import { everyStmtList } from "../../../packages/compiler/src/ir/traverse.js"; -import { computeMayThrow } from "../../../packages/compiler/src/backend/c/may-throw.js"; +import { computeMayThrow } from "../../../packages/compiler/src/backend/may-throw.js"; import { computeTraced } from "../../../packages/compiler/src/backend/cycle-analysis.js"; try { diff --git a/tests/fixtures/self-hosting/c-emitter.ts b/tests/fixtures/self-hosting/c-emitter.ts deleted file mode 100644 index f287c6d65..000000000 --- a/tests/fixtures/self-hosting/c-emitter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js"; -import { emitCModule } from "../../../packages/compiler/src/backend/c/c-emitter.js"; - -// Exercise the production entry point, including scalarization, metadata, -// ownership analysis, helper discovery and complete translation-unit output. -const module = deserializeModule(readFileSync(process.argv[2]!, "utf8")); -const sources = new Map(); -if (process.argv.length > 4) { - const inputs = JSON.parse(readFileSync(process.argv[4]!, "utf8")) as { file: string; text: string }[]; - for (const input of inputs) sources.set(input.file, input.text); -} -writeFileSync(process.argv[3]!, emitCModule(module, undefined, { debugSources: sources })); diff --git a/tests/fixtures/self-hosting/emitter-literals.ts b/tests/fixtures/self-hosting/emitter-literals.ts index f135fee5f..621dba17c 100644 --- a/tests/fixtures/self-hosting/emitter-literals.ts +++ b/tests/fixtures/self-hosting/emitter-literals.ts @@ -1,5 +1,4 @@ import { commentText, llvmBytes, llvmQuoted, octalByte, unsignedHex } from "../../../packages/compiler/src/backend/literals.js"; -import { cCommentText, cDecl, cNumberLiteral, cStringLiteral, cType } from "../../../packages/compiler/src/backend/c/types.js"; import { f64Lit, ffiNativeTypeLl, llvmCommentText } from "../../../packages/compiler/src/backend/llvm/common.js"; import { mangleClassNew, mangleClassObj, mangleClassRelease, mangleClassRetain, mangleClassStruct, mangleField, mangleFunction, mangleGlobal, mangleLocal, mangleRecordStruct, mangleVtInstance } from "../../../packages/compiler/src/backend/mangle.js"; import { BOOL, F64, STRING, VOID, arrayOf, funcOf, mapOf, type IrType } from "../../../packages/compiler/src/ir/ir.js"; @@ -8,7 +7,6 @@ const bytes = new Uint8Array(256); for (let i = 0; i < bytes.length; i++) bytes[i] = i; console.log(llvmBytes(bytes)); console.log(llvmBytes(bytes, false)); -console.log(cStringLiteral(Buffer.from(bytes))); console.log(llvmBytes(new Uint8Array())); for (let i = 0; i < 256; i++) console.log(unsignedHex(i), octalByte(i)); @@ -21,14 +19,12 @@ for (const value of [-1, 0.5, NaN, Infinity]) { } for (const value of [0, -0, 1, -1, 0.1, Number.MIN_VALUE, Number.MAX_VALUE, Infinity, -Infinity, NaN]) { - console.log(f64Lit(value), cNumberLiteral(value)); + console.log(f64Lit(value)); } for (const text of ["", "ordinary", "é日本😀", 'quote"slash\\\0', "line\nnext\u2028last", "/* comment */"]) { console.log(llvmQuoted(text)); - console.log(cStringLiteral(Buffer.from(text, "utf8"))); console.log(JSON.stringify(commentText(text))); - console.log(JSON.stringify(cCommentText(text))); console.log(JSON.stringify(llvmCommentText(text))); } @@ -38,21 +34,6 @@ for (const name of ["ordinary", "x.12", "$x%y", "é日本", "😀", "nul\0", "da console.log(mangleClassRetain(name), mangleClassRelease(name), mangleVtInstance(name), mangleRecordStruct(name)); } -const types: IrType[] = [ - F64, BOOL, STRING, VOID, arrayOf(F64), mapOf(STRING, F64), - { kind: "set", elem: STRING }, { kind: "record", shapeId: "row" }, - { kind: "object", className: "User" }, { kind: "union", unionId: "choice" }, - { kind: "classval", className: "User" }, { kind: "bytes", elem: "u8" }, - { kind: "dyn" }, { kind: "jsval" }, { kind: "caught" }, - { kind: "promise", inner: STRING }, funcOf([STRING], F64), - { kind: "generator", yieldT: F64, retT: STRING, nextT: VOID }, - { kind: "date" }, { kind: "bigint" }, { kind: "regex" }, - { kind: "url" }, { kind: "searchParams" }, { kind: "symbol" }, - { kind: "stats" }, { kind: "fileHandle" }, { kind: "spawnRes" }, - { kind: "child" }, { kind: "netServer" }, { kind: "netSocket" }, - { kind: "http2Session" }, { kind: "http2Stream" }, -]; -for (const type of types) console.log(cType(type), cDecl(type, "slot")); console.log(ffiNativeTypeLl("f64"), ffiNativeTypeLl("bool"), ffiNativeTypeLl("u8")); console.log(ffiNativeTypeLl("u32"), ffiNativeTypeLl("i32"), ffiNativeTypeLl("cstring"), ffiNativeTypeLl("void")); try { console.log(ffiNativeTypeLl("bytes")); } diff --git a/tests/fixtures/self-hosting/runtime-features.ts b/tests/fixtures/self-hosting/runtime-features.ts index 5ec714661..fb97dd70d 100644 --- a/tests/fixtures/self-hosting/runtime-features.ts +++ b/tests/fixtures/self-hosting/runtime-features.ts @@ -1,27 +1,6 @@ import { readFileSync } from "node:fs"; import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js"; -import { - moduleUsesRegex, moduleUsesCopying, moduleUsesLegacyTextDecoder, - moduleUsesFileHandle, moduleUsesFetch, moduleUsesProcessEvents, - moduleUsesEmitter, moduleUsesStream, moduleUsesZlib, moduleUsesDc, - moduleUsesAssert, moduleUsesDynInvoke, moduleUsesDynAsync, - moduleUsesInspect, moduleUsesChildProcess, moduleUsesNet, - moduleUsesSymbol, moduleUsesBigInt, moduleUsesSearchParams, moduleUsesQs, - moduleUsesParseArgs, moduleUsesFsWatch, moduleUsesNodeTest, - moduleUsesDgram, moduleUsesHttpServer, moduleUsesHttp2, moduleUsesTls, moduleUsesTlsCa, -} from "../../../packages/compiler/src/ir/ir.js"; +import { moduleRuntimeFeatures } from "../../../packages/compiler/src/ir/ir.js"; const mod = deserializeModule(readFileSync(process.argv[2]!, "utf8")); -console.log(JSON.stringify({ - regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod), - legacyTextDecoder: moduleUsesLegacyTextDecoder(mod), fileHandle: moduleUsesFileHandle(mod), - fetch: moduleUsesFetch(mod), processEvents: moduleUsesProcessEvents(mod), - emitter: moduleUsesEmitter(mod), stream: moduleUsesStream(mod), - zlib: moduleUsesZlib(mod), dc: moduleUsesDc(mod), assert: moduleUsesAssert(mod), - dynInvoke: moduleUsesDynInvoke(mod), dynAsync: moduleUsesDynAsync(mod), - inspect: moduleUsesInspect(mod), childProcess: moduleUsesChildProcess(mod), net: moduleUsesNet(mod), - symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), searchParams: moduleUsesSearchParams(mod), - qs: moduleUsesQs(mod), parseArgs: moduleUsesParseArgs(mod), fsWatch: moduleUsesFsWatch(mod), - nodeTest: moduleUsesNodeTest(mod), dgram: moduleUsesDgram(mod), http: moduleUsesHttpServer(mod), - http2: moduleUsesHttp2(mod), tls: moduleUsesTls(mod), tlsCa: moduleUsesTlsCa(mod), -})); +console.log(JSON.stringify(moduleRuntimeFeatures(mod))); diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index 187a341e9..b2538c119 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -64,7 +64,7 @@ function signatureAt(checker: SemanticChecker, node: AstNode): SemanticSignature return signature; } -/** This runs both in Node and in the C/LLVM native executables. Each fact +/** This runs both in Node and in native LLVM executables. Each fact * checks relationships within the live native parser/checker's response; * server-assigned numeric handles never become hard-coded test answers. */ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: SemanticChecker, file: AstFile): void { diff --git a/tests/fixtures/self-hosting/union-conversions.ts b/tests/fixtures/self-hosting/union-conversions.ts index 8f5fc1e9c..3f33311e3 100644 --- a/tests/fixtures/self-hosting/union-conversions.ts +++ b/tests/fixtures/self-hosting/union-conversions.ts @@ -153,6 +153,36 @@ scenario("s", STRING, "constructor", "__proto__"); scenario("n", F64, 0, -2.5); scenario("b", BOOL, false, true); scenario("i", STRING, "kept", "width", true); + +// Large conversions exercise indexed identity lookup with the same semantic +// discriminants as small conversions. Reverse tags and execute every arm. +const largeFrom: IrUnionDef = { id: "large-from", arms: [], discriminant: { field: "kind", cases: [] } }; +const largeTo: IrUnionDef = { id: "large-to", arms: [], discriminant: { field: "kind", cases: [] } }; +for (let index = 0; index < 32; index++) { + const shape = `large-${index}`; + records.push({ id: shape, fields: [{ name: "kind", type: STRING }, { name: "value", type: F64 }] }); + largeFrom.arms.push(recordType(shape)); + largeFrom.discriminant!.cases.push({ tag: index, values: [shape] }); + largeTo.arms.unshift(recordType(shape)); + largeTo.discriminant!.cases.push({ tag: 31 - index, values: [shape] }); +} +unions.push(largeFrom, largeTo); +const largePlan = planUnionRetag(largeFrom, largeTo, shapeOf, widthLift); +if (!largePlan) throw new Error("missing large conversion"); +functions.push(buildUnionRetag("large-retag", largeFrom, largeTo, largePlan, loc, applyLift, () => "payload")); +for (let index = 0; index < largeFrom.arms.length; index++) { + const type = largeFrom.arms[index]!; + if (type.kind !== "record") throw new Error("bad large conversion source"); + const source: IrExpr = { kind: "recordLit", type, loc, fields: [ + { name: "kind", value: string(type.shapeId) }, { name: "value", value: number(index) }, + ] }; + const converted: IrExpr = { kind: "call", callee: "large-retag", args: [wrap(largeFrom, index, source)], type: unionType(largeTo.id), loc }; + body.push(print(`large:${index}`, { + kind: "recordGet", obj: { kind: "unionNarrow", unionId: largeTo.id, tag: 31 - index, value: converted, type, loc }, + shapeId: type.shapeId, field: "value", type: F64, loc, + })); +} + // The same native stage emits checked single-arm extraction and deferred // scalar field reads. Wrong non-unit tags must never share a payload read. const scalarUnion: IrUnionDef = { id: "scalar", arms: [BOOL, F64, STRING, UNDEFINED_T] }; diff --git a/tests/harness/README.md b/tests/harness/README.md index bf21d61e0..2c8fa951d 100644 --- a/tests/harness/README.md +++ b/tests/harness/README.md @@ -4,13 +4,13 @@ Two lanes over the same suite: plain (`pnpm test`) and sanitized (`SCRIPTC_SAN=1 One deliberate exception to raw byte-compare: `node:test` programs (tests/harness/node-test.test.ts over tests/fixtures/node-test) cannot live in the corpus because Node's spec reporter embeds a real duration in EVERY result line — no node:test program has deterministic stdout, under Node itself included. Those fixtures still run both lanes against the Node oracle, but with one documented normalization applied to both sides (durations, stack frames, the inspect property block); everything else — symbols, indentation, directives, summary counts, the failing-section "test at" locations and error messages — must match byte-exactly, plus exit-code parity against the fixture's `// @exit:` line. Fixtures never console.log inside test bodies: Node's reporter stream lags console output racily, so mixed programs aren't byte-comparable against any oracle. -The LLVM backend rides the same two lanes through its own dual-backend differential (tests/harness/llvm-differential.test.ts): every corpus program is ATTEMPTED through `--backend=llvm`; programs the tier claims must be byte-identical through both backends AND against the Node oracle (stdout always, stderr for exit-0 programs, exit codes), and programs outside the tier must refuse with exactly one SC3001 diagnostic naming the first unsupported IR construct — never wrong code, never a silent fallback. Tier membership is auto-discovered (attempt + catch the refusal), the survey's six trivial-tier programs are pinned as a floor, and the run prints the claimed count plus the refusal histogram (the next phase's queue). Under `SCRIPTC_SAN=1` the emitted .ll's `sanitize_address` attribute opts the LLVM-emitted functions into ASan instrumentation too. +The LLVM differential suite (`tests/harness/llvm-differential.test.ts`) compiles every corpus program in release and dev modes and compares stdout, successful stderr, and exit status with Node. Any backend refusal fails the test. Under `SCRIPTC_SAN=1`, the emitted functions and runtime are instrumented with AddressSanitizer. -A third, env-gated lane runs the corpus AND the fixture sets with runtime legs (tests/fixtures/server, tests/fixtures/dgram, tests/fixtures/fetch) on LINUX: `SCRIPTC_LINUX=1 pnpm exec vitest run tests/harness/linux-differential.test.ts` cross-compiles every program via `zig cc` (`SCRIPTC_CC=zigcc`/`SCRIPTC_TARGET` in native-toolchain.ts) and byte-compares against a Linux Node oracle inside a Docker container — for the fixtures, both lanes, the per-case driver, and the fetch servers all run in-container. `SCRIPTC_LINUX_TARGET=-linux-gnu.2.36` runs the whole lane in Bookworm; `SCRIPTC_LINUX_TARGET=-linux-musl` runs it in Alpine. The container platform follows the triple (`linux/arm64` for AArch64, `linux/amd64` for x86_64; the latter uses Rosetta/qemu on Apple-silicon Docker). It skips entirely without the env var and is never part of the commit gate. +A third, env-gated lane runs the corpus AND the fixture sets with runtime legs (tests/fixtures/server, tests/fixtures/dgram, tests/fixtures/fetch) on LINUX: `SCRIPTC_LINUX=1 pnpm exec vitest run tests/harness/linux-differential.test.ts` emits every program through LLVM, selects a precompiled runtime pack with `SCRIPTC_TARGET`, and links through Zig and byte-compares against a Linux Node oracle inside a Docker container — for the fixtures, both lanes, the per-case driver, and the fetch servers all run in-container. `SCRIPTC_LINUX_TARGET=-linux-gnu.2.36` runs the whole lane in Bookworm; `SCRIPTC_LINUX_TARGET=-linux-musl` runs it in Alpine. The container platform follows the triple (`linux/arm64` for AArch64, `linux/amd64` for x86_64; the latter uses Rosetta/qemu on Apple-silicon Docker). It skips entirely without the env var and is never part of the commit gate. A fourth lane does the same on WINDOWS: `SCRIPTC_WIN=1 pnpm exec vitest run tests/harness/windows-differential.test.ts` cross-compiles for `x86_64-windows-gnu`, ships each .exe (plus the program source) to the Windows box over scp, runs BOTH sides there over ssh — the box's own Windows Node is the oracle — and byte-compares stdout/exit codes with nothing normalized. `SCRIPTC_WIN_FILTER=` narrows a run; the box alias is `windows-dev` (`SCRIPTC_WIN_HOST` overrides). Programs needing cross-gated features skip with the gate's reason, and the in-file `WINDOWS_SKIPS` list names what compiles but deliberately diverges on Windows (posix-shaped spawn programs whose /bin children are ENOENT on Windows Node too, uid/tty surfaces) — both lists are the port's worklist. Never part of the commit gate. -A fifth lane covers LIBRARY MODE across targets: `SCRIPTC_CROSS=1 pnpm exec vitest run tests/harness/library-cross.test.ts` cross-builds every K-fixture library profile (both emissions) for `aarch64-linux-gnu.2.36`, `x86_64-linux-gnu.2.36`, `aarch64-linux-musl`, `x86_64-linux-musl`, `x86_64-windows-gnu`, and `x86_64-macos`, then asserts per archive ON THE HOST: K1 symbol exactness (nm reads ELF/COFF/Mach-O alike), the K8 ambient audit, and that each fixture's probe LINKS against the target's libc — plus, on win32, the documented embedder system libs (advapi32/iphlpapi/ws2_32, native-toolchain.ts's unconditional executable set). With `SCRIPTC_LINUX=1`/`SCRIPTC_WIN=1` additionally set, the K2 scalar probe also EXECUTES per linux triple in its matching Docker distribution and on the Windows box; x86_64-macos is build-only by contract (the probe runs as a bonus when Rosetta is present). Needs zig on PATH; `SCRIPTC_CROSS_FILTER=` narrows the fixture list. Never part of the commit gate. +A fifth lane covers LIBRARY MODE across targets: `SCRIPTC_CROSS=1 pnpm exec vitest run tests/harness/library-cross.test.ts` cross-builds every K-fixture library profile (LLVM emission) for `aarch64-linux-gnu.2.36`, `x86_64-linux-gnu.2.36`, `aarch64-linux-musl`, `x86_64-linux-musl`, `x86_64-windows-gnu`, and `x86_64-macos`, then asserts per archive ON THE HOST: K1 symbol exactness (nm reads ELF/COFF/Mach-O alike), the K8 ambient audit, and that each fixture's probe LINKS against the target's libc — plus, on win32, the documented embedder system libs (advapi32/iphlpapi/ws2_32, native-toolchain.ts's unconditional executable set). With `SCRIPTC_LINUX=1`/`SCRIPTC_WIN=1` additionally set, the K2 scalar probe also EXECUTES per linux triple in its matching Docker distribution and on the Windows box; x86_64-macos is build-only by contract (the probe runs as a bonus when Rosetta is present). Needs zig on PATH; `SCRIPTC_CROSS_FILTER=` narrows the fixture list. Never part of the commit gate. ## Workflow @@ -27,7 +27,7 @@ without a real fixture or registered generated scenario fails the profile suite. `pnpm test:fetch-conformance` generates a program from that profile and runs it -under the pinned Node plus both native backends. The default seed exercises +under the pinned Node and the LLVM backend. The default seed exercises WebIDL argument conversion/order, AbortSignal events, and twelve valid ReadableStream state-machine traces. Reproduce or widen a campaign with: @@ -77,19 +77,17 @@ This is a local latency benchmark, not a timing assertion in CI. Compare the sam ### Cache layers -Test runs are dominated by clang (~275 corpus programs × two lanes at -O2/-O1+ASan). The production content-addressed build cache and the harness's oracle cache make repeat runs fast. Tests pin them under `node_modules/.cache/scriptc-tests/cas` (gitignored; override with `SCRIPTC_CACHE_DIR`) instead of using the per-user default: +The compiler caches frontend work, native program objects, and linked outputs. Tests place caches under `node_modules/.cache/scriptc-tests/cas` (gitignored; override with `SCRIPTC_CACHE_DIR`) instead of the per-user default. Cached programs still execute for every differential comparison. -- **binaries** (`bin/`, native-toolchain.ts): key = resolved clang identity/version + target/compiler environment + implicit system-header dependency bytes + linker/assembler identities + runtime fingerprint (every runtime .c/.h + the vendor pin) + the full normalized command line + the emitted C bytes (byte-stable by project invariant). A hit skips native code generation and linking; the binary still RUNS live, so no comparison or sanitizer coverage is ever skipped. Each hit is checksum-verified. The sanitized lane's flags land in naturally distinct keys. FFI archive/object inputs and ambient system libraries always relink because their named files can hide mutable transitive dependencies. -- **library archives** (`lib/`, native-toolchain.ts): key = resolved clang and archiver identities/versions + target/compiler environment and implicit dependencies + runtime fingerprint + target/flags + gated runtime-source set + emitted program-TU bytes. A checksum-verified hit skips native code generation and `ar`. -- **library program objects** (`program-obj/`, native-toolchain.ts): generated library TUs compile into checksum-verified objects keyed independently of the tiny exact-source identity TU. A build-id-only miss reuses the large program object, compiles the identity getters, and rearchives; runtime/header/toolchain inputs remain part of the key and are rechecked before publication. -- **early executable frontend** (`early-exe/`, executable/executable-cache.ts): exact executable repeats validate a fresh effective-compiler identity, the frontend's complete file/resolution snapshot, and the native dependency proof before restoring the emitted C/LLVM unit, optional IR, and final executable without spawning TypeScript, lowering, or performing full clang/linker discovery. Source/config/package edits, newly appearing resolution candidates, mode/target/compiler/FFI changes, runtime/toolchain updates, and corrupt payloads miss; a valid frontend hit with an invalid native proof restores only the TU and falls through to compileC's strict native cache. -- **early library frontend** (`early-lib/`, library/library-cache.ts): exact library repeats validate content hashes for every file the TypeScript frontend read plus recorded failed-resolution and directory-enumeration probes, then restore the generated C/LLVM unit, optional IR, sidecar, and native feature gates without spawning TypeScript or lowering again. TypeScript comment-only misses may restore compressed lowered IR after token equivalence checks; source locations and exact-source sidecar/build identities regenerate from current bytes. Semantic comments/directives, JavaScript comments, token/config/package edits, and newly appearing resolution candidates miss. The native archive tier still performs its own toolchain/runtime checks. -- **runtime objects** (`obj/`, native-toolchain.ts): per-flavor .o for the runtime sources, including a distinct `-DSCR_LIB` flavor, so an edited executable or library recompiles only the program's own translation unit before linking or archiving. Each object carries a verified digest; a damaged entry is rebuilt before it reaches the linker or archiver. Publication rechecks the runtime and implicit-toolchain fingerprints after compilation so a concurrent source/header edit cannot place new bytes under an old key. Compiles route through ccache when installed, silently falling back when not. -- **oracle results** (`oracle/`, differential.test.ts): Node's stdout/exit per program, keyed by program bytes + the spawned node's version + shim contents + invocation shape. Only the spawn is skipped; the comparison never changes. Real-time programs (setTimeout/setInterval/Promise.race — 18 of 298) are excluded and always spawn Node live: their stdout is a timer interleave that Node and the native binary only agree on under the same instantaneous load, so a cached verdict from one run must never meet a live native run from another. +- **frontend results** (`early-exe/` and `early-lib/`): exact repeats validate the frontend's complete file and resolution snapshot before restoring emitted LLVM, optional IR, and native feature selections. Source, configuration, package-resolution, target, and FFI changes invalidate the relevant result. Library comment-only edits can reuse lowered IR while regenerating source locations and identity metadata. +- **LLVM artifacts** (`native-codegen-v1/`): checksum-verified program objects and assembly are keyed by LLVM input, helper identity, target ABI, and optimization settings. Library identity getters compile separately, and dev libraries can reuse unchanged LLVM shards. +- **runtime packs**: ordinary builds select and verify installed precompiled objects and vendor archives. These are distribution artifacts, so compiling an application does not compile runtime C sources. +- **development toolchain caches** (`bin/`, `lib/`, `program-obj/`, `obj/`): explicit sanitizer and runtime development builds retain dependency-verified caches for external LLVM/C compilation, instrumented runtime objects, and native linking. Their compiler, header, archiver, and linker inputs participate in cache identities. +- **oracle results** (`oracle/`): deterministic Node outputs are keyed by program inputs, the spawned Node version, shim contents, and invocation shape. Timing-sensitive programs always run a live oracle. Escape hatches: `SCRIPTC_NO_CACHE=1` bypasses every cache in both directions (no reads, no writes — the run behaves exactly like the uncached path). An explicitly empty `SCRIPTC_CACHE_DIR` does the same; a non-empty value overrides the production default. Eviction is a size-capped LRU sweep over each cache root (`SCRIPTC_CACHE_MAX_MB`, default 4096), run after the first write and periodically in long-lived processes; reads bump mtimes. -Compiler environment variables that can resolve mutable compilation inputs (`CPATH`, `SDKROOT`, clang config directories, and their peers) conservatively bypass persistent artifacts and runtime objects. Compiler wrappers do the same because they can inject inputs conditionally on the real source/object topology; direct Clang, Apple's system Clang shim, and `zig cc` retain caching. The compiler must remain available so every invocation rediscovers dependency selection. Opaque archiver wrappers rebuild library program members and archives while retaining runtime-object reuse; trusted platform archivers and `zig ar` retain complete archive hits. Link-only search variables and explicit native link inputs bypass complete executables but retain safe runtime-object reuse. +For the external development toolchain, environment variables that can resolve mutable compilation inputs (`CPATH`, `SDKROOT`, clang config directories, and their peers) conservatively bypass persistent artifacts and runtime objects. Compiler wrappers do the same because they can inject inputs conditionally on the real source/object topology; direct Clang, Apple's system Clang shim, and `zig cc` retain caching. The compiler must remain available so every invocation rediscovers dependency selection. Opaque archiver wrappers rebuild library program members and archives while retaining runtime-object reuse; trusted platform archivers and `zig ar` retain complete archive hits. Link-only search variables and explicit native link inputs bypass complete executables but retain safe runtime-object reuse. `pnpm test:cache-identity` (optionally `--san`) is the acceptance artifact: it runs the full suite uncached, cache-populating, and cached, then diffs every test's name/status/failure output between the cached and uncached passes and exits nonzero on any drift. diff --git a/tests/harness/cache-identity.mjs b/tests/harness/cache-identity.mjs index fa80252dd..d12a37d6b 100644 --- a/tests/harness/cache-identity.mjs +++ b/tests/harness/cache-identity.mjs @@ -5,9 +5,9 @@ * Identical output is the gate for trusting the cache; any drift exits 1 * with the differing lines. * - * Usage: node tests/harness/cache-identity.mjs [--san] [--keep-cache] + * Usage: node tests/harness/cache-identity.mjs [--san] [--keep-llvmache] * --san run the sanitized lane (SCRIPTC_SAN=1) - * --keep-cache don't wipe the cache before the populate pass + * --keep-llvmache don't wipe the cache before the populate pass * Prints a timing table (uncached / populate / cached) as a side effect. */ import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; @@ -17,7 +17,7 @@ import { fileURLToPath } from "node:url"; const repoRoot = join(fileURLToPath(import.meta.url), "../../.."); const san = process.argv.includes("--san"); -const keepCache = process.argv.includes("--keep-cache"); +const keepLlvmache = process.argv.includes("--keep-llvmache"); // Must match vitest.config.ts's default. const cacheDir = process.env.SCRIPTC_CACHE_DIR ?? join(repoRoot, "node_modules/.cache/scriptc-tests/cas"); const outDir = mkdtempSync(join(tmpdir(), "scr-cache-identity-")); @@ -57,7 +57,7 @@ function counts(json) { return `${json.numTotalTests} tests, ${json.numPassedTests} passed, ${json.numFailedTests} failed, ${json.numPendingTests} skipped`; } -if (!keepCache) { +if (!keepLlvmache) { console.log(`wiping cache at ${cacheDir}`); rmSync(cacheDir, { recursive: true, force: true }); } diff --git a/tests/harness/console-io.test.ts b/tests/harness/console-io.test.ts index 5377c27fd..0948e497d 100644 --- a/tests/harness/console-io.test.ts +++ b/tests/harness/console-io.test.ts @@ -40,7 +40,7 @@ async function build(name: string): Promise<{ binary: string; sourceFile: string outPath: join(outDir, name), outDir, sanitize, - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 181ebd3ae..63659204a 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -71,6 +71,22 @@ test.each([ } }); +test("coverage preserves function-local class capture environments", () => { + const { coverage } = analyze(join(repoRoot, "tests/corpus/local-class-unknown.ts")); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.stats.statementsFailed).toBe(0); + expect(coverage.unreached?.diagnostics ?? []).toEqual([]); +}); + +test("module stream aliases remain static in separately declared functions", () => { + const { coverage } = analyze(join(repoRoot, "tests/corpus/stdio-write-errors.mjs")); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.runtimeFences ?? []).toEqual([]); + expect(coverage.stats.statementsFailed).toBe(0); +}); + test("runtime-optional refined unions report blockers without crashing", () => { for (const dynamic of [false, true]) { const { coverage } = analyze(join(repoRoot, "tests/diagnostics/runtime-optional-refined-union.ts"), { dynamic }); diff --git a/tests/harness/deadstrip.test.ts b/tests/harness/deadstrip.test.ts index a488cbfd2..7db7ec404 100644 --- a/tests/harness/deadstrip.test.ts +++ b/tests/harness/deadstrip.test.ts @@ -19,23 +19,21 @@ const repoRoot = join(import.meta.dirname, "../.."); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test("unreached bodies leave no trace in the emitted C", async () => { +test("unreached bodies leave no trace in the emitted LLVM", async () => { const outDir = join(cacheDir, `deadstrip-c${sanitize ? "-san" : ""}`); const result = await compile(join(repoRoot, "tests/corpus/420-dead-strip-modules/main.ts"), { outPath: join(outDir, "program"), outDir, sanitize, - // Pinned: this test greps the emitted C for reached/unreached names — - // it measures the C backend's artifact by design. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); } - const c = readFileSync(result.cPath, "utf8"); + const c = readFileSync(result.llvmPath, "utf8"); // Reached bodies are there (mangled names embed the source name)... for (const reached of ["used", "double", "evenSteps", "oddSteps", "usedMethod", "tag"]) { - expect(c, `expected reached '${reached}' in the emitted C`).toContain(reached); + expect(c, `expected reached '${reached}' in the emitted LLVM`).toContain(reached); } // ...unreached ones are not: no function body, no async spawn wrapper or // trampoline (unusedAsync), no monomorphized instance (unusedGeneric), @@ -47,7 +45,7 @@ test("unreached bodies leave no trace in the emitted C", async () => { "unusedGeneric", "unusedMethod", ]) { - expect(c, `unreached '${gone}' must leave no trace in the emitted C`).not.toContain(gone); + expect(c, `unreached '${gone}' must leave no trace in the emitted LLVM`).not.toContain(gone); } }); diff --git a/tests/harness/dgram.test.ts b/tests/harness/dgram.test.ts index 95597778f..ca0cf8797 100644 --- a/tests/harness/dgram.test.ts +++ b/tests/harness/dgram.test.ts @@ -91,7 +91,7 @@ async function build(entry: string): Promise { // Pinned: a networking suite whose flake surface is already the // sockets — the compiled lane stays the C reference so any diff is // network behavior, never a backend-lane change. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index f03d82236..8e3181f38 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -356,10 +356,7 @@ async function compileAndRun(file: string): Promise { outDir, sanitize, dynamic, - // Pinned: this suite IS the C-reference lane — its meaning is "the C - // backend matches Node", regardless of what the product default does. - // llvm-differential.test.ts owns the LLVM lane over the same corpus. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 1566e57f5..8b8e29b36 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -52,7 +52,7 @@ async function compileAndRun(name: string, source: string, ext: "ts" | "cjs" = " // Pinned: the exact TypeError text and path rendering of failed checked // casts are C-reference pins; lane identity stays fixed so a diff means // the dyn boundary changed, never that the default backend moved. - const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "c", dynamic }); + const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "llvm", dynamic }); if (!result.ok) { throw new Error( "dyncheck program failed to compile:\n" + diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 951ff7460..4f7bc38b7 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -65,10 +65,7 @@ async function compileAndRun( // for runtime-fence shapes only mixed dynamic graphs can spell (the // diagnostics suite's directive, applied to the run-and-observe lane). const dynamic = /^\/\/ @dynamic\s*$/.test(source.split("\n", 1)[0] ?? ""); - // Pinned: the uncaught-STDERR line shape (SEMANTICS.md divergence 11) is - // pinned against the C reference; the LLVM lane's uncaught epilogue is - // llvm-differential's parity job, not this suite's. - const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "c", dynamic }); + const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "llvm", dynamic }); if (!result.ok) { throw new Error( "errors program failed to compile:\n" + diff --git a/tests/harness/event-loop.test.ts b/tests/harness/event-loop.test.ts index 94f7c309f..64b6230c4 100644 --- a/tests/harness/event-loop.test.ts +++ b/tests/harness/event-loop.test.ts @@ -62,7 +62,7 @@ async function compileFixture(name: string): Promise { // Pinned: piped-stdin timing fixtures were written against the C // lane; lane identity stays fixed so a diff means the stdin/event-loop // story changed, never that the default backend moved. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( diff --git a/tests/harness/fetch-conformance.test.ts b/tests/harness/fetch-conformance.test.ts index e4b416f74..cff2003d4 100644 --- a/tests/harness/fetch-conformance.test.ts +++ b/tests/harness/fetch-conformance.test.ts @@ -6,7 +6,7 @@ * surface manifest. The reflected inventory supplies the denominator. This * suite checks that the pinned Node executable is the intended oracle, every * profile row names differential evidence, and the generated - * WebIDL/state-machine program agrees through both native backends. + * WebIDL/state-machine program agrees through LLVM. */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -121,7 +121,7 @@ async function run(command: string, args: string[]): Promise { } } -async function build(backend: "c" | "llvm"): Promise { +async function build(backend: "llvm"): Promise { const outDir = join( workRoot, `${sourceHash}-${backend}-${sanitize ? "san" : "plain"}`, @@ -394,7 +394,7 @@ describe( `generated fetch conformance (seed=${conformanceSeed}, traces=${conformanceTraceCount}` + `${sanitize ? ", sanitized" : ""})`, () => { - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "%s backend matches the pinned Node oracle", async (backend) => { const binary = await build(backend); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index b3f4d045b..a731d91fc 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -126,7 +126,7 @@ async function build(entry: string): Promise { // Pinned: real-socket fixtures — the compiled lane stays the C // reference so a diff is fetch behavior, never a backend-lane change // (npm.test.ts rides the default and covers the fallback at scale). - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( @@ -139,7 +139,7 @@ async function build(entry: string): Promise { async function buildStatic( entry: string, - backend: "c" | "llvm", + backend: "llvm", flavor = "", ): Promise { const hash = createHash("sha256"); @@ -172,7 +172,7 @@ const cases = globSync(join(fixturesRoot, "cases/*/main.ts")) .map((entry) => ({ name: entry.split("/").at(-2)!, entry })); describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { - test.for(["dynamic", "c", "llvm"] as const)( + test.for(["dynamic", "llvm"] as const)( "RequestInit wrapper spread / %s backend", async (lane) => { const entry = join(fixturesRoot, "spread-wrapper/main.mts"); @@ -199,7 +199,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { ] as const; test.for( staticCases.flatMap((name) => - (["c", "llvm"] as const).map((backend) => [name, backend] as const), + (["llvm"] as const).map((backend) => [name, backend] as const), ), )("%s / %s backend", async ([name, backend]) => { const entry = join( @@ -224,7 +224,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 120_000); - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "static fetch / %s backend settles an abandoned compressed response", async (backend) => { const entry = join(fixturesRoot, "static-abandon/main.mts"); @@ -239,7 +239,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { 120_000, ); - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "static fetch / %s backend trusts NODE_EXTRA_CA_CERTS", async (backend) => { const certs = join(fixturesRoot, "../server/certs"); @@ -311,9 +311,8 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { fixturesRoot, "static-env-snapshot/main.mts", ); - const [dynamic, c, llvm] = await Promise.all([ + const [dynamic, llvm] = await Promise.all([ build(entry), - buildStatic(entry, "c", "env-snapshot"), buildStatic(entry, "llvm", "env-snapshot"), ]); const argv = [ @@ -339,7 +338,6 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { const [nodeRes, ...nativeResults] = await Promise.all([ runBinary("node", [entry, ...argv], env), runBinary(dynamic, argv, env), - runBinary(c, argv, env), runBinary(llvm, argv, env), ]); for (const nativeRes of nativeResults) { @@ -358,7 +356,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { 120_000, ); - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "static fetch / %s backend supports IPv6 URL literals and NO_PROXY", async (backend) => { const server = createHttpServer( @@ -466,23 +464,21 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { `http://127.0.0.1:${redirectAddress.port}/to-blocked-port`; const entry = join(fixturesRoot, "static-bad-port/main.mts"); - const [dynamic, c, llvm] = await Promise.all([ + const [dynamic, llvm] = await Promise.all([ build(entry), - buildStatic(entry, "c", "bad-port"), buildStatic(entry, "llvm", "bad-port"), ]); const argv = [blockedUrl, redirectUrl]; const [nodeRes, ...nativeResults] = await Promise.all([ runBinary("node", [entry, ...argv]), runBinary(dynamic, argv), - runBinary(c, argv), runBinary(llvm, argv), ]); for (const nativeRes of nativeResults) { expect(nativeRes.stdout.equals(nodeRes.stdout)).toBe(true); expect(nativeRes.exitCode).toBe(nodeRes.exitCode); } - expect(redirectRequests).toBe(4); + expect(redirectRequests).toBe(3); expect(blockedRequests).toBe(0); } finally { await Promise.all( @@ -505,7 +501,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { process.env["SCRIPTC_FETCH_CURL"] = "1"; try { const entry = join(fixturesRoot, "static/main.mts"); - await buildStatic(entry, "c", "curl-env"); + await buildStatic(entry, "llvm", "curl-env"); } finally { if (previous === undefined) { delete process.env["SCRIPTC_FETCH_CURL"]; @@ -572,7 +568,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` ]); }, 120_000); - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "static fetch / %s backend authenticates to http_proxy", async (backend) => { const entry = join(fixturesRoot, "static-proxy/main.mts"); @@ -602,7 +598,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` 120_000, ); - test.for(["dynamic", "c", "llvm"] as const)( + test.for(["dynamic", "llvm"] as const)( "%s fetch authenticates over an HTTPS proxy transport", async (lane) => { const entry = join(fixturesRoot, "static-proxy/main.mts"); @@ -667,9 +663,8 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` throw new Error("missing HTTPS address"); } const entry = join(fixturesRoot, "static-network-error/main.mts"); - const [dynamic, c, llvm] = await Promise.all([ + const [dynamic, llvm] = await Promise.all([ build(entry), - buildStatic(entry, "c", "https-proxy-reject"), buildStatic(entry, "llvm", "https-proxy-reject"), ]); const argv = [`https://localhost:${address.port}`]; @@ -685,7 +680,6 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` const [nodeRes, ...nativeResults] = await Promise.all([ runBinary("node", [entry, ...argv], env), runBinary(dynamic, argv, env), - runBinary(c, argv, env), runBinary(llvm, argv, env), ]); for (const nativeRes of nativeResults) { @@ -702,7 +696,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` 120_000, ); - test.for(["dynamic", "c", "llvm"] as const)( + test.for(["dynamic", "llvm"] as const)( "%s fetch rejects an invalid opted-in proxy instead of dialing directly", async (lane) => { const entry = join(fixturesRoot, "static-proxy/main.mts"); @@ -753,7 +747,7 @@ describe(`proxy env opt-in (NODE_USE_ENV_PROXY${sanitize ? ", sanitized" : ""})` expect(servers.proxiedRequests() - before).toBe(0); }, 120_000); - test.for(["c", "llvm"] as const)( + test.for(["llvm"] as const)( "static fetch / %s backend honors wildcard no_proxy exclusions", async (backend) => { const entry = join(fixturesRoot, "static-network/main.mts"); diff --git a/tests/harness/ffi-callback-pool.test.ts b/tests/harness/ffi-callback-pool.test.ts index 53f925d97..d8f7a75c8 100644 --- a/tests/harness/ffi-callback-pool.test.ts +++ b/tests/harness/ffi-callback-pool.test.ts @@ -9,7 +9,7 @@ const fixture = join(import.meta.dirname, "../ffi"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; const oracle = process.env["SCRIPTC_NODE_FFI_ORACLE"]; -describe.each(["c", "llvm"] as const)("static native callback pool, %s", backend => { +describe.each(["llvm"] as const)("static native callback pool, %s", backend => { test.skipIf(process.platform === "win32")("refuses foreign-thread invocation before touching a script closure", async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-ffi-callback-thread-")); const object = join(outDir, "wide.o"); diff --git a/tests/harness/ffi-catalog.test.ts b/tests/harness/ffi-catalog.test.ts index f5dabf347..ccbdf4843 100644 --- a/tests/harness/ffi-catalog.test.ts +++ b/tests/harness/ffi-catalog.test.ts @@ -9,7 +9,7 @@ const fixture = join(import.meta.dirname, "../ffi"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; const oracle = process.env["SCRIPTC_NODE_FFI_ORACLE"]; -describe.each(["c", "llvm"] as const)("static node:ffi catalog, %s", backend => { +describe.each(["llvm"] as const)("static node:ffi catalog, %s", backend => { test("binds native symbols and preserves independent library lifetimes", async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-ffi-catalog-")); const object = join(outDir, "wide.o"); diff --git a/tests/harness/ffi-frameworks.test.ts b/tests/harness/ffi-frameworks.test.ts index 5d6854c5b..397403dc5 100644 --- a/tests/harness/ffi-frameworks.test.ts +++ b/tests/harness/ffi-frameworks.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { compile } from "@scriptc/compiler"; -describe.skipIf(process.platform !== "darwin").each(["c", "llvm"] as const)("FFI framework links, %s", backend => { +describe.skipIf(process.platform !== "darwin").each(["llvm"] as const)("FFI framework links, %s", backend => { test("links a framework symbol without an extra object or library input", async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-ffi-framework-")); const entry = join(outDir, "app.ts"); diff --git a/tests/harness/ffi-memory.test.ts b/tests/harness/ffi-memory.test.ts index d4c2ae728..c44d67f34 100644 --- a/tests/harness/ffi-memory.test.ts +++ b/tests/harness/ffi-memory.test.ts @@ -16,7 +16,7 @@ const expected = "bigint true\n10,99,30,40 99,30\n10,99,30,40 55,99,30,40\ntrue\ "RangeError ERR_OUT_OF_RANGE The length is too large\nError ERR_BUFFER_TOO_LARGE Buffer is too large\n" + "true\ntrue\ntrue\ntrue\nfalse\nfalse\n"; -describe.each(["c", "llvm"] as const)("native FFI memory, %s", backend => { +describe.each(["llvm"] as const)("native FFI memory, %s", backend => { test("aliases native memory and copies on request without an engine", async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-ffi-memory-")); // Both loader spellings must keep real storage; bare `require` also diff --git a/tests/harness/ffi-scalars.test.ts b/tests/harness/ffi-scalars.test.ts index 230ffa2ea..a6b650bfc 100644 --- a/tests/harness/ffi-scalars.test.ts +++ b/tests/harness/ffi-scalars.test.ts @@ -9,7 +9,7 @@ const fixture = join(root, "tests/ffi"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; const scalarClasses = ["f32", "i8", "u16", "i16"] as const; -describe.each(["c", "llvm"] as const)("native FFI scalar widths, %s", (backend) => { +describe.each(["llvm"] as const)("native FFI scalar widths, %s", (backend) => { test("matches Node conversions through native calls, callbacks, and mixed register/stack arguments without an engine", async () => { const outDir = join(root, "node_modules/.cache/scriptc-tests/ffi-scalars", sanitize ? "san" : "plain", backend); mkdirSync(outDir, { recursive: true }); diff --git a/tests/harness/ffi-wide.test.ts b/tests/harness/ffi-wide.test.ts index 1bb49d375..24f76e7a9 100644 --- a/tests/harness/ffi-wide.test.ts +++ b/tests/harness/ffi-wide.test.ts @@ -8,7 +8,7 @@ import { compile } from "@scriptc/compiler"; const fixture = join(import.meta.dirname, "../ffi"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -describe.each(["c", "llvm"] as const)("native FFI wide integers and pointers, %s", backend => { +describe.each(["llvm"] as const)("native FFI wide integers and pointers, %s", backend => { test("preserves exact values through calls, retained callbacks, and mixed ABI arguments without an engine", async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-ffi-wide-")); const object = join(outDir, "wide.o"); diff --git a/tests/harness/ffi.test.ts b/tests/harness/ffi.test.ts index 69a0e2d6e..2f65062de 100644 --- a/tests/harness/ffi.test.ts +++ b/tests/harness/ffi.test.ts @@ -1,6 +1,6 @@ /* Outbound native FFI is an integration lane rather than a corpus case: * Node has no equivalent static-link surface to differential-run. The same - * TypeScript and native archive run through BOTH scriptc backends, and the + * TypeScript and native archive run through the LLVM backend, and the * result bytes must match. The fixture covers every value ABI class, * integer coercion, embedded-NUL/UTF-8 string spans, byte spans, format-2 * raw/context callbacks, format-3 callback cstring/span copies (lossy UTF-8, @@ -86,7 +86,7 @@ const expected = [ "", ].join("\n"); -describe.each(["c", "llvm"] as const)("outbound native FFI, %s backend", (backend) => { +describe.each(["llvm"] as const)("outbound native FFI, %s backend", (backend) => { test("calls the manifest-bound archive across every v1 ABI class plus v2/v3/v4/v5 callback ABI classes", async () => { const outDir = join(cacheRoot, backend); mkdirSync(outDir, { recursive: true }); @@ -795,7 +795,7 @@ test.each([ }, ); -describe.each(["c", "llvm"] as const)("FFI binding identity, %s backend", (backend) => { +describe.each(["llvm"] as const)("FFI binding identity, %s backend", (backend) => { test("a local same-named function remains ordinary TypeScript with Node-byte parity", async () => { const outDir = join(cacheRoot, `shadow-${backend}`); mkdirSync(outDir, { recursive: true }); @@ -866,7 +866,7 @@ describe.each(["c", "llvm"] as const)("FFI binding identity, %s backend", (backe }); }); -describe.each(["c", "llvm"] as const)("retained FFI at process exit, %s backend", (backend) => { +describe.each(["llvm"] as const)("retained FFI at process exit, %s backend", (backend) => { test("process 'exit' listeners can release retained registrations after a normal loop drain", async () => { const outDir = join(cacheRoot, `retained-exit-listener-${backend}`); mkdirSync(outDir, { recursive: true }); @@ -949,7 +949,7 @@ describe.each(["c", "llvm"] as const)("retained FFI at process exit, %s backend" }); }); -describe.each(["c", "llvm"] as const)("retained FFI release traps, %s backend", (backend) => { +describe.each(["llvm"] as const)("retained FFI release traps, %s backend", (backend) => { test("releasing a closure that was never registered traps precisely", async () => { const outDir = join(cacheRoot, `retained-missing-${backend}`); mkdirSync(outDir, { recursive: true }); diff --git a/tests/harness/fs-write-sync.test.ts b/tests/harness/fs-write-sync.test.ts index 6483dd20e..f682947c1 100644 --- a/tests/harness/fs-write-sync.test.ts +++ b/tests/harness/fs-write-sync.test.ts @@ -20,7 +20,7 @@ describe.skipIf(process.platform === "win32")( outPath: join(outDir, "sigpipe"), outDir, sanitize, - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index 04a21d3e9..b81211f0f 100644 --- a/tests/harness/inheritance.test.ts +++ b/tests/harness/inheritance.test.ts @@ -20,8 +20,8 @@ const repoRoot = join(import.meta.dirname, "../.."); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -/** Compiles an inline program and returns the emitted C text. */ -async function emittedC(name: string, source: string, ext = "ts"): Promise { +/** Compiles an inline program and returns the emitted LLVM text. */ +async function emittedLlvm(name: string, source: string, ext = "ts"): Promise { const key = createHash("sha256") .update(source) .update(sanitize ? "san" : "plain") @@ -31,21 +31,20 @@ async function emittedC(name: string, source: string, ext = "ts"): Promise `${d.code}: ${d.message}`).join("\n"), ); } - return readFileSync(result.cPath, "utf8"); + return readFileSync(result.artifact.path, "utf8"); } describe("inheritance codegen", () => { test("standalone classes emit no vtable machinery", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "standalone", `class Point { x: number; @@ -60,13 +59,12 @@ const p = new Point(3); console.log(p.norm()); `, ); - expect(c).not.toContain("ScrVt"); - expect(c).not.toContain("->vt"); - expect(c).not.toContain("sc_vtable_"); + expect(llvm).not.toMatch(/%sc_vtt_(?:Point|Gauge)\b/); + expect(llvm).not.toContain("sc_vtable_"); }); test("a never-overridden method in a hierarchy stays a direct call", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "devirt", `class Animal { name: string; @@ -90,16 +88,16 @@ console.log(a.id(), a.speak()); `, ); // The devirtualized call is a direct sc_f_ call of Animal's id... - expect(c).toMatch(/sc_f__x25_Animal_id\(/); + expect(llvm).toMatch(/sc_f__x25_Animal_id\(/); // ...and id never becomes a vtable slot, while speak does and the // base-typed call site dispatches through it. - expect(c).not.toContain("sc_vs_id"); - expect(c).toContain("sc_vs_speak"); - expect(c).toMatch(/->vt\)->sc_vs_speak\(/); + expect(llvm).toMatch(/%sc_vtt_Animal = type \{ %ScrVt, ptr \}/); + expect(llvm).toMatch(/@sc_vtable_Dog = [^\n]*ptr @sc_f__x25_Dog_speak/); + expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Animal, ptr %\w+, i64 0, i32 1[\s\S]*call ptr %\w+\(ptr /); }); test("accessors on standalone classes stay zero-cost direct calls", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "accessor-standalone", `class Gauge { _level: number; @@ -119,17 +117,16 @@ g.level += 2; console.log(g.level); `, ); - expect(c).not.toContain("ScrVt"); - expect(c).not.toContain("->vt"); - expect(c).not.toContain("sc_vtable_"); + expect(llvm).not.toMatch(/%sc_vtt_(?:Point|Gauge)\b/); + expect(llvm).not.toContain("sc_vtable_"); // Reads and writes are direct calls of the accessor functions // ("get:level" mangles ':' as _x3a_). - expect(c).toMatch(/sc_f__x25_Gauge_get_x3a_level\(/); - expect(c).toMatch(/sc_f__x25_Gauge_set_x3a_level\(/); + expect(llvm).toMatch(/sc_f__x25_Gauge_get_x3a_level\(/); + expect(llvm).toMatch(/sc_f__x25_Gauge_set_x3a_level\(/); }); test("the get and set halves of accessors devirtualize independently", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "accessor-devirt", `class Cell { _v: number; @@ -157,17 +154,16 @@ console.log(c.v, c.label); `, ); // The overridden getter dispatches through its slot... - expect(c).toContain("sc_vs_get_x3a_label"); - expect(c).toMatch(/->vt\)->sc_vs_get_x3a_label\(/); + expect(llvm).toMatch(/%sc_vtt_Cell = type \{ %ScrVt, ptr \}/); + expect(llvm).toMatch(/@sc_vtable_LoudCell = [^\n]*ptr @sc_f__x25_LoudCell_get_x3a_label/); + expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Cell, ptr %\w+, i64 0, i32 1[\s\S]*call ptr %\w+\(ptr /); // ...while the never-overridden pair keeps direct calls and no slots. - expect(c).not.toContain("sc_vs_get_x3a_v"); - expect(c).not.toContain("sc_vs_set_x3a_v"); - expect(c).toMatch(/sc_f__x25_Cell_get_x3a_v\(/); - expect(c).toMatch(/sc_f__x25_Cell_set_x3a_v\(/); + expect(llvm).toMatch(/sc_f__x25_Cell_get_x3a_v\(/); + expect(llvm).toMatch(/sc_f__x25_Cell_set_x3a_v\(/); }); test("a stream subclass embeds the ScrStream prefix and delegates its state RC", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "stream-subclass", `import { Readable } from "node:stream"; class Counter extends Readable { @@ -183,14 +179,14 @@ r.on("data", (b: Buffer) => console.log(b.toString())); ); // The subclass struct carries the full ScrStream prefix (registry, // display name, state pointer) ahead of user fields... - expect(c).toMatch(/ScrStreamState \*sc_st;[\s\S]{0,200}\/\* n \*\//); + expect(llvm).toContain("%sc_o_Counter = type { i64, ptr, ptr, ptr, ptr, double }"); // ...its teardown delegates the state block to the runtime... - expect(c).toContain("scr_stream_st_release(o->sc_st)"); + expect(llvm).toMatch(/getelementptr inbounds %sc_o_Counter, ptr %o, i64 0, i32 4[\s\S]*call void @scr_stream_st_release\(ptr /); // ...and super() initializes the state over the allocated struct. - expect(c).toContain("scr_stream_init_readable((ScrStream *)"); + expect(llvm).toContain("call void @scr_stream_init_readable(ptr "); }); - test("a runtime-fenced nested class extending a stream still compiles to C", async () => { + test("a runtime-fenced nested stream class emits the refusal without class helpers", async () => { // The phase-1 reachable bug: a class declared inside a block (a // runtime fence in JS) whose instances are captured emitted capture- // box RC adapters for the uncollected class — a C compile error. @@ -198,7 +194,7 @@ r.on("data", (b: Buffer) => console.log(b.toString())); // the inert f64 placeholder before emission (no instance can exist; // every use traps), so the capture box is a PLAIN-kind box and the // class never reaches the emitter at all. - const c = await emittedC( + const llvm = await emittedLlvm( "stream-nested-fence", `const { Readable } = require("stream"); { @@ -212,13 +208,15 @@ r.on("data", (b: Buffer) => console.log(b.toString())); `, "js", ); - expect(c).not.toContain("sc_retain_R_v"); - expect(c).not.toContain("uncollected class"); - expect(c).toMatch(/sc_l_stream_0 = scr_box_new\(SCR_BOX_\w+\)/); + expect(llvm).not.toContain("sc_retain_R_v"); + expect(llvm).not.toContain("uncollected class"); + expect(llvm).not.toContain("%sc_o_R"); + expect(llvm).toMatch(/%sc_l_stream_0 = alloca ptr/); + expect(llvm).toContain("call void @scr_throw_error_msg_code"); }); test("a getter-only override of a pair fills the set slot with a thrower", async () => { - const c = await emittedC( + const llvm = await emittedLlvm( "accessor-shadow", `class Box { _v: number; @@ -249,8 +247,9 @@ console.log(b.v); // JS shadowing: the derived class's synthesized setter occupies the // set slot (a base-typed write must throw like Node), so BOTH halves // dispatch dynamically here. - expect(c).toContain("sc_vs_get_x3a_v"); - expect(c).toContain("sc_vs_set_x3a_v"); - expect(c).toMatch(/sc_f__x25_SealedBox_set_x3a_v/); + expect(llvm).toMatch(/%sc_vtt_Box = type \{ %ScrVt, ptr, ptr \}/); + expect(llvm).toMatch(/@sc_vtable_SealedBox = [^\n]*ptr @sc_f__x25_SealedBox_get_x3a_v, ptr @sc_f__x25_SealedBox_set_x3a_v/); + expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Box, ptr %\w+, i64 0, i32 2/); + expect(llvm).toMatch(/sc_f__x25_SealedBox_set_x3a_v/); }); }); diff --git a/tests/harness/installed-entry.test.ts b/tests/harness/installed-entry.test.ts index 97093ba14..781ca4bf9 100644 --- a/tests/harness/installed-entry.test.ts +++ b/tests/harness/installed-entry.test.ts @@ -26,7 +26,7 @@ test.each([ // Node refuses native TS stripping inside node_modules. Its oracle // runs the identical source tree at the original corpus location. const oracle = await execFileAsync(process.execPath, [join(original, entryName)]); - for (const backend of ["c", "llvm"] as const) { + for (const backend of ["llvm"] as const) { const outDir = join(dir, backend); const built = await compile(entry, { outDir, outPath: join(outDir, "program"), backend, sanitize }); expect(built.ok, !built.ok ? JSON.stringify(built.diagnostics) : "").toBe(true); diff --git a/tests/harness/island.test.ts b/tests/harness/island.test.ts index f2ba8573e..6d3dc2fb8 100644 --- a/tests/harness/island.test.ts +++ b/tests/harness/island.test.ts @@ -11,7 +11,7 @@ * the engine's stack check on every entry. Unbounded recursion in an * island eval must surface as a catchable RangeError — a crash here * means the re-anchor regressed. - * - The static/dynamic fence: --dynamic must not change emitted C, and a + * - The static/dynamic fence: --dynamic must not change emitted LLVM, and a * static binary must stay in its size class (the ~620KB engine must never * leak into default builds). * @@ -40,7 +40,7 @@ interface RunResult { interface BuildResult { binaryPath: string; - cPath: string; + llvmPath: string; } /** Compiles an inline program (island tests default to --dynamic). */ @@ -66,10 +66,7 @@ async function build( outDir, sanitize: san, dynamic, - // Pinned: the static/dynamic fence and the size classes below are - // assertions ON the emitted C and the C-lane binary — this suite - // measures the C backend's artifact by design. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( @@ -77,7 +74,7 @@ async function build( result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), ); } - return { binaryPath: result.binaryPath, cPath: result.cPath }; + return { binaryPath: result.binaryPath, llvmPath: result.llvmPath }; } /** Linux ASan prints a once-per-process warning to stderr the first time a @@ -338,7 +335,7 @@ console.log(__island_eval("Promise.reject(new TypeError('island second')); 'arme expect(r.stderr).toBe("Unhandled promise rejection: RangeError: static first\n"); }); - test("--dynamic does not change emitted C for island-free programs", async () => { + test("--dynamic does not change emitted LLVM for island-free programs", async () => { const source = `function greet(who: string): string { return "hello " + who; } @@ -351,7 +348,7 @@ console.log(greet("world"), 6 * 7); build("same-c", source, { dynamic: true }), ]); const body = (r: BuildResult) => - readFileSync(r.cPath, "utf8").replaceAll(dirname(r.cPath), "OUTDIR"); + readFileSync(r.llvmPath, "utf8").replaceAll(dirname(r.llvmPath), "OUTDIR"); expect(body(dyn)).toBe(body(stat)); }); diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index bfe4d9f54..ee35d748a 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -25,7 +25,7 @@ async function surfaceOf(name: string, source: string): Promise { outPath: join(outDir, "program"), outDir, emitIr: true, - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); diff --git a/tests/harness/library-callbacks.test.ts b/tests/harness/library-callbacks.test.ts index f23060d38..dee328e72 100644 --- a/tests/harness/library-callbacks.test.ts +++ b/tests/harness/library-callbacks.test.ts @@ -80,8 +80,8 @@ const localizationTest = const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-callbacks", flavor); -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; interface BuildOpts { sanitize?: boolean; diff --git a/tests/harness/library-contract.test.ts b/tests/harness/library-contract.test.ts index 659b5d94c..11b839935 100644 --- a/tests/harness/library-contract.test.ts +++ b/tests/harness/library-contract.test.ts @@ -38,8 +38,8 @@ const fixtureRoot = join(repoRoot, "tests/library-mode"); const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-contract", flavor); -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; const TOP_LEVEL_ORDER = [ "format", "wire_version", "abi_version", "compiler_version", "entry", @@ -66,7 +66,7 @@ async function buildContract( * directories is exactly what the canonical root-relative paths * guarantee, and the V13-style assertions prove it). */ root = cacheDir, -): Promise<{ outDir: string; archive: string; cPath: string; sidecarPath: string; doc: SidecarDoc; bytes: Buffer }> { +): Promise<{ outDir: string; archive: string; llvmPath: string; sidecarPath: string; doc: SidecarDoc; bytes: Buffer }> { const dir = join(fixtureRoot, fixture); const outDir = join(root, `${fixture}-${emission}${tag}`); mkdirSync(outDir, { recursive: true }); @@ -92,7 +92,7 @@ async function buildContract( return { outDir, archive: result.archivePath, - cPath: result.cPath, + llvmPath: result.llvmPath, sidecarPath: result.sidecarPath!, doc: JSON.parse(bytes.toString("utf8")) as SidecarDoc, bytes: bytes as Buffer, @@ -113,7 +113,7 @@ function nmDefined(archive: string, prefix: string): string[] { describe.each(EMISSIONS)("contract sidecar, %s emission", (emission) => { test("anti-alphabetical declaration order, schema shape, V11/V12 identity", async () => { - const { outDir, archive, cPath, doc, bytes } = await buildContract("contract", emission); + const { outDir, archive, llvmPath, doc, bytes } = await buildContract("contract", emission); // The emitter's own self-check ran before writing; the test-side // validator agrees the document conforms. @@ -247,7 +247,7 @@ describe.each(EMISSIONS)("contract sidecar, %s emission", (emission) => { ...(emission === "llvm" ? ["-Wno-override-module"] : ["-Wno-comment", "-I", join(repoRoot, "packages/runtime/src")]), - "-c", cPath, + "-c", llvmPath, "-o", keptObject, ]); expect(nmDefined(keptObject, "kc_")).toEqual(doc.abi.exports.map((s) => `kc_${s}`).sort()); @@ -741,7 +741,7 @@ async function sidecarProjection(source: string): Promise { profile_format: 1, name: "sidecar-projection-fixture", entry: "lib.ts", - emission: "c", + emission: "llvm", abi: { prefix: "kp_", init_symbol: "kp_init", @@ -869,7 +869,7 @@ async function sidecarRefusal( profile_format: 1, name: "sidecar-refusal-fixture", entry: "lib.ts", - emission: "c", + emission: "llvm", abi: { prefix: "ks_", init_symbol: "ks_init", diff --git a/tests/harness/library-cross.test.ts b/tests/harness/library-cross.test.ts index 7cdb8a3d1..f1a1868b3 100644 --- a/tests/harness/library-cross.test.ts +++ b/tests/harness/library-cross.test.ts @@ -17,9 +17,8 @@ * x86_64-macos (build-only; the host arm64-macos build is * the ordinary suites' tested baseline) * - * Both emissions build per target (the K suite's reference/differential - * posture — emitted .c and .ll deliberately carry no target triple, so - * `zig cc -target` compiles either). Per archive, ON THE HOST (no + * LLVM program objects combine with the target's precompiled runtime + * pack. Per archive, ON THE HOST (no * execution — Apple's nm is llvm-nm and reads ELF/COFF/Mach-O alike): * * - K1 cross: prefix-carrying external definitions equal the profile's @@ -49,7 +48,7 @@ * present the probe runs as a bonus (detected, never required). * * Cost (warm zig cache, M-series host): ~1s per archive build, ~2.5min - * for the full 112-build matrix — plus zig on PATH as a hard requirement, + * for the complete matrix — plus zig on PATH as a hard requirement, * which is why the lane is env-gated rather than part of the default * suite. SCRIPTC_CROSS_FILTER= narrows the fixture list for * triage. Never part of the commit gate. */ @@ -91,8 +90,8 @@ type Target = (typeof TARGETS)[number]; * link line; the probe links pin the set. */ const WIN32_EMBEDDER_LIBS = ["-ladvapi32", "-liphlpapi", "-lws2_32"]; -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; const filter = process.env["SCRIPTC_CROSS_FILTER"]; /* Every K fixture that IS a library profile (int-corpus and npm-refuse @@ -338,14 +337,14 @@ describe.skipIf(!enabled)("cross-target library conformance", () => { // Box etiquette (the windows lane's): everything in ONE directory, // created per run and deleted at the end — a lane-distinct name so // a concurrent windows-differential run is never disturbed. - const dirWin = "C:\\Users\\rdp\\work\\scriptc-xlib-lane"; + const dirWin = `C:\\Users\\rdp\\work\\scriptc-xlib-${process.pid}-${Date.now()}`; const { archive, outDir } = await buildLibrary("scalars", emission, "x86_64-windows-gnu"); const probe = linkProbe("scalars", archive, outDir, "x86_64-windows-gnu"); const ssh = (cmd: string): string => execFileSync("ssh", ["-o", "ConnectTimeout=15", host, cmd], { encoding: "utf8", timeout: 120_000 }); try { ssh(`cmd /c if not exist ${dirWin} mkdir ${dirWin}`); - execFileSync("scp", ["-q", probe, `${host}:C:/Users/rdp/work/scriptc-xlib-lane/probe-${emission}.exe`], { + execFileSync("scp", ["-q", probe, `${host}:${dirWin.replaceAll("\\", "/")}/probe-${emission}.exe`], { timeout: 120_000, }); const out = ssh(`cd /d ${dirWin} && probe-${emission}.exe`); diff --git a/tests/harness/library-int.test.ts b/tests/harness/library-int.test.ts index e23fded18..a819da2e9 100644 --- a/tests/harness/library-int.test.ts +++ b/tests/harness/library-int.test.ts @@ -46,8 +46,8 @@ const fixtureRoot = join(repoRoot, "tests/library-mode"); const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-int", flavor); -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; /* ── the corpus (reference package §3, sources verbatim) ───────────────── * `body` is the case's program text: top-level statements, or the body of @@ -453,7 +453,7 @@ export function grow(a: number): void { } `; const c: CorpusCase = { name: "render", body: "", param: false, expected: "refuse" }; - const profile = corpusProfile(c, "c", "lib.ts") as { exports: object[] }; + const profile = corpusProfile(c, "llvm", "lib.ts") as { exports: object[] }; profile.exports.push( { export: "half", symbol: "kc_half", params: ["f64"], returns: "void" }, { export: "grow", symbol: "kc_grow", params: ["f64"], returns: "void" }, @@ -497,7 +497,7 @@ function sidecarProfile(integerSlots: object[], patch: Record = profile_format: 1, name: "int-sidecar", entry: "lib.ts", - emission: "c", + emission: "llvm", abi: { prefix: "ks_", init_symbol: "ks_init", diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 33e818ec5..74fa0ecd4 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -85,8 +85,8 @@ const platformTest = process.env["SCRIPTC_PORTABLE_ONLY"] === "1" ? test.skip : const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-mode", flavor); -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; interface BuildOpts { sanitize?: boolean; @@ -535,7 +535,7 @@ let refusalCounter = 0; async function refusal( source: string, profilePatch: Record, - emission: Emission = "c", + emission: Emission = "llvm", ): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { const outDir = join(cacheDir, `refusal-${refusalCounter++}`); mkdirSync(outDir, { recursive: true }); @@ -573,7 +573,7 @@ async function refusal( async function acceptance( source: string, profilePatch: Record, - emission: Emission = "c", + emission: Emission = "llvm", ): Promise { const outDir = join(cacheDir, `acceptance-${refusalCounter++}`); mkdirSync(outDir, { recursive: true }); @@ -679,7 +679,7 @@ async function npmRefusal( profile_format: 1, name: "npm-refusal-fixture", entry: join(fixtureRoot, "npm-refuse", entryFile), - emission: "c", + emission: "llvm", abi: { prefix: "kx_", init_symbol: "kx_init", @@ -903,7 +903,7 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { }); test("unreached fences compile at zero cost: the emitted code is byte-identical", async () => { - // ONE entry file feeds both builds (emitted C/LLVM embeds the entry's + // ONE entry file feeds both builds (emitted LLVM embeds the entry's // path in provenance comments; the fence's cost is what's measured, // not the build directory's spelling). const entryDir = join(cacheDir, `fence-zerocost-${emission}`); @@ -935,7 +935,7 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { ); const result = await compileLibrary({ profilePath, outDir }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - emitted.push(readFileSync(result.cPath, "utf8")); + emitted.push(readFileSync(result.llvmPath, "utf8")); } expect(emitted[1]).toBe(emitted[0]); }); diff --git a/tests/harness/library-multi.test.ts b/tests/harness/library-multi.test.ts index f668d7452..eaf70e402 100644 --- a/tests/harness/library-multi.test.ts +++ b/tests/harness/library-multi.test.ts @@ -146,8 +146,8 @@ const sanitize = process.env["SCRIPTC_SAN"] === "1"; const flavor = sanitize ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-multi", flavor); -type Emission = "llvm" | "c"; -const EMISSIONS: Emission[] = ["llvm", "c"]; +type Emission = "llvm"; +const EMISSIONS: Emission[] = ["llvm"]; /** Build one instance's localized archive for one emission: the fixture * profile is patched (emission flipped, entry made absolute) into the @@ -270,10 +270,8 @@ function buildProbe(archiveA: string, archiveB: string, outDir: string, tag: str const PAIRINGS: { tag: string; a: Emission; b: Emission }[] = [ { tag: "llvm-llvm", a: "llvm", b: "llvm" }, - { tag: "c-c", a: "c", b: "c" }, // Two embedder builds need not share a backend: one archive per emission // links and runs the same. - { tag: "llvm-c", a: "llvm", b: "c" }, ]; describe.each(PAIRINGS)("two instances, one process ($tag)", ({ tag, a, b }) => { @@ -626,7 +624,7 @@ localizationTest("M7: thread-instanced and runtime-localized archives compose in if (nmTool === null) ctx.skip("no nm/llvm-nm on PATH for the symbol-exactness check"); const [archiveT, archiveB] = await Promise.all([ buildThreaded("llvm", { localize: true }), - buildInstance("b", "c"), + buildInstance("b", "llvm"), ]); // The composed archive's link surface stays exactly the declared set: // thread-local storage adds no external definitions (M1's one Darwin @@ -849,7 +847,6 @@ describe.skipIf(!crossOn)("cross-target localization", () => { test.skipIf(!linuxOn).for([ ["aarch64-linux-gnu.2.36", "llvm"], ["x86_64-linux-gnu.2.36", "llvm"], - ["x86_64-linux-gnu.2.36", "c"], ["aarch64-linux-musl", "llvm"], ["x86_64-linux-musl", "llvm"], ] as const)( @@ -1087,7 +1084,7 @@ test.each(MOBILE_TARGETS)( if (!result.ok) { expect(result.diagnostics[0]!.code).toBe("SC3002"); expect(result.diagnostics[0]!.message).toContain(target); - expect(result.diagnostics[0]!.message).toContain("SCRIPTC_CC=zigcc scriptc build --lib --profile "); + expect(result.diagnostics[0]!.message).toContain("scriptc build --lib --profile "); } }); }, @@ -1155,14 +1152,16 @@ function buildAppleProbe(archives: string[], source: string, tag: string, target * is reused; otherwise the first available iPhone boots headlessly and the * suite shuts it down again. */ let bootedSimulator: { udid: string; bootedByUs: boolean } | null = null; -type SimulatorDevice = { udid: string; state: string; name: string }; +type SimulatorDevice = { udid: string; state: string; name: string; deviceTypeIdentifier?: string }; type SimulatorListing = { devices: Record }; function availableIphoneSimulators(listing: SimulatorListing): SimulatorDevice[] { return Object.entries(listing.devices) .filter(([runtime]) => runtime.includes(".SimRuntime.iOS-")) .flatMap(([, devices]) => devices) - .filter((device) => device.name.startsWith("iPhone")); + .filter((device) => device.deviceTypeIdentifier === undefined + ? device.name.startsWith("iPhone") + : device.deviceTypeIdentifier.startsWith("com.apple.CoreSimulator.SimDeviceType.iPhone-")); } test("M12: simulator reuse ignores booted devices from non-iOS runtimes", () => { @@ -1173,11 +1172,12 @@ test("M12: simulator reuse ignores booted devices from non-iOS runtimes", () => ], "com.apple.CoreSimulator.SimRuntime.iOS-26-0": [ { udid: "phone", state: "Shutdown", name: "iPhone 17" }, + { udid: "renamed", state: "Shutdown", name: "Test Device", deviceTypeIdentifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro" }, { udid: "tablet", state: "Booted", name: "iPad Pro" }, ], }, }); - expect(devices.map((device) => device.udid)).toEqual(["phone"]); + expect(devices.map((device) => device.udid)).toEqual(["phone", "renamed"]); }); function shutdownOwnedSimulator(): void { diff --git a/tests/harness/linux-differential.test.ts b/tests/harness/linux-differential.test.ts index 2be15bd9c..b2fe47a3b 100644 --- a/tests/harness/linux-differential.test.ts +++ b/tests/harness/linux-differential.test.ts @@ -254,10 +254,7 @@ async function crossCompileAndRun(file: string): Promise { const key = hash.update("linux\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - // Pinned "c" here and at every compile below: the Linux lane is a - // C-reference suite (the cross-compile story is the C backend's; the - // LLVM lane's Linux coverage runs in llvm-differential on the sandbox). - const result = await compile(file, { outPath: join(outDir, "program"), outDir, dynamic: wantsDynamic(file), backend: "c" }); + const result = await compile(file, { outPath: join(outDir, "program"), outDir, dynamic: wantsDynamic(file), backend: "llvm" }); if (!result.ok) { throw new Error( "corpus program failed to cross-compile:\n" + @@ -344,7 +341,7 @@ async function crossCompileFixture(entry: string): Promise { const key = hash.update("linux\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - const result = await compile(entry, { outPath: join(outDir, "program"), outDir, backend: "c" }); + const result = await compile(entry, { outPath: join(outDir, "program"), outDir, backend: "llvm" }); if (!result.ok) { throw new Error( "fixture failed to cross-compile:\n" + @@ -472,7 +469,7 @@ async function crossCompileNpmCase(entry: string): Promise { const key = hash.update("linux\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - const result = await compile(entry, { outPath: join(outDir, "program"), outDir, dynamic: true, backend: "c" }); + const result = await compile(entry, { outPath: join(outDir, "program"), outDir, dynamic: true, backend: "llvm" }); if (!result.ok) { throw new Error( "npm case failed to cross-compile:\n" + @@ -495,7 +492,7 @@ async function crossCompileFetchFixture(entry: string): Promise { const key = hash.update("linux\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - const result = await compile(entry, { outPath: join(outDir, "program"), outDir, dynamic: true, backend: "c" }); + const result = await compile(entry, { outPath: join(outDir, "program"), outDir, dynamic: true, backend: "llvm" }); if (!result.ok) { throw new Error( "fetch fixture failed to cross-compile:\n" + diff --git a/tests/harness/llvm-differential.test.ts b/tests/harness/llvm-differential.test.ts index 3ced17511..5c4d9a3c7 100644 --- a/tests/harness/llvm-differential.test.ts +++ b/tests/harness/llvm-differential.test.ts @@ -1,28 +1,13 @@ -/* The dual-backend differential: tier membership is AUTO-DISCOVERED by - * attempting the LLVM build on every corpus program. A program the tier - * claims must be byte-identical through BOTH backends — stdout (always), - * stderr (exit-0 programs), exit code — and both must match the Node - * oracle. A program outside the tier must REFUSE loudly under the - * explicit `backend: "llvm"` pin this suite builds with: diagnostic - * SC3001 naming the first unsupported IR construct, never wrong code. - * (The RELEASE default is LLVM with a transparent C fallback; the pin is - * exactly how this suite keeps refusals loud. Each refused program is - * additionally rebuilt through the default lane below, asserting the - * fallback lands on the C backend with the same refusal kind recorded.) - * - * SCRIPTC_SAN=1 rebuilds both lanes with ASan + the runtime RC audit; the - * emitted .ll opts its functions into instrumentation via sanitize_address, - * so the LLVM-emitted frames are covered too. The refusal histogram and - * the claimed count print at the end — phase 2's queue. - */ +/* Both LLVM optimization modes must compile every corpus program and match + * Node's stdout, successful stderr, and expected exit status. Any backend + * refusal is a failure. SCRIPTC_SAN=1 instruments emitted code and runtime. */ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { globSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { release as osRelease } from "node:os"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; -import { afterAll, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { shardSelect, shardSuffix } from "./shard.js"; @@ -43,42 +28,18 @@ const files = shardSelect( (f) => f.slice(corpusDir.length + 1), ); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -const helperOnly = process.env["SCRIPTC_LLVM_HELPER_ONLY"] === "1"; +const requestedMode = process.env["SCRIPTC_LLVM_TEST_MODE"]; +if (requestedMode !== undefined && requestedMode !== "release" && requestedMode !== "dev") { + throw new Error("SCRIPTC_LLVM_TEST_MODE must be release or dev when set"); +} +// Combined CI lanes cover release in differential.test.ts. Standalone and +// packaged-artifact runs retain both modes unless one is explicitly selected. +const optimizationModes: readonly ("release" | "dev")[] = requestedMode === undefined + ? ["release", "dev"] : [requestedMode]; // Same known-env contract as the main differential suite. process.env["SCRIPTC_TEST_ENV"] = "from-harness"; -/** The survey's six in-tier programs: the tier floor. Auto-discovery may - * claim MORE (it does — strings/globals joined in phase 1), but these six - * regressing out of the tier is always a bug, so they are pinned. */ -const TIER_FLOOR = [ - "001-hello.ts", - "002-log-args.ts", - "100-number-format.ts", - "101-arithmetic.ts", - "400-fib.ts", - "401-mutual-recursion.ts", -]; - -/** Fixed backend gaps whose corpus programs must remain in the LLVM tier. */ -const TIER_REGRESSIONS = [ - "2672-http-request-response-callback.ts", -]; - -/** Phase 2 object-emission floor: these claimed corpus cases pin every - * required helper-validation family. TLS server creation remains one of the - * six explicit SC3001 refusals outside this floor. */ -const HELPER_TIER_FLOOR = [ - "1020-async-basics.ts", - "2010-generators-basics.ts", - "984-exceptions-finally.ts", - "1100-island-eval-basics.ts", - "1200-regex-test-basics.ts", - "2672-http-request-response-callback.ts", - "1404-zlib-crypto-bytes.ts", - "2557-tls-ca-store.ts", -]; - interface RunResult { stdout: Buffer; stderr: Buffer; @@ -206,177 +167,33 @@ function programInputs(file: string): string[] { ].sort(); } -async function build(file: string, backend: "c" | "llvm" | "helper" | "default") { +async function build(file: string, optimization: "release" | "dev") { const hash = createHash("sha256"); for (const f of programInputs(file)) hash.update(f).update(readFileSync(f)); - // "llvm-c" (not the bare key differential.test.ts computes) keeps this - // suite's C-lane binaries in their OWN cache slots: compile() always - // rewrites its output, so sharing paths with the main differential - // means one suite can relink a binary the other is exec'ing — Linux - // answers ETXTBSY (observed on the sandbox lane as claims grew). - const key = hash - .update(sanitize ? "san" : "plain") - .update(wantsDynamic(file) ? "dyn" : "") - .update(backend === "llvm" ? "llvm" : backend === "helper" ? "llvm-helper" : backend === "c" ? "llvm-c" : "llvm-def") - .digest("hex") - .slice(0, 16); + const key = hash.update(sanitize ? "san" : "plain") + .update(wantsDynamic(file) ? "dyn" : "").update("llvm-" + optimization).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - // The binary BASENAME must be unique per lane (and per flavor): fs-corpus - // programs derive their scratch paths from tail(process.argv[1]) — the - // basename — so two concurrently running native binaries that share a - // name share a scratch directory and corrupt each other's fs state. This - // suite runs its LLVM and C binaries concurrently (the Promise.all - // below), and the main differential's C binary ("program") for the same - // fixture can run in a sibling worker at the same time; the node oracle - // is safe on its own (argv[1] is the .ts path). Observed on the public - // CI runner as the seven fs/path llvm-differential failures of run - // 29965245855 — empty or interleaved stdout on whichever lane lost the - // race, reproducible locally by racing the two same-named binaries. - const lane = backend === "llvm" ? "program-llvm" : backend === "helper" ? "program-llvmhelper" : backend === "c" ? "program-llvmc" : "program-llvmdef"; return compile(file, { - outPath: join(outDir, `${lane}${sanitize ? "-san" : ""}`), - outDir, - sanitize, - dynamic: wantsDynamic(file), - // "default" leaves the option unset — the release default's - // LLVM-with-transparent-C-fallback lane, exercised on refusals below. - ...(backend === "default" ? {} : { backend: backend === "helper" ? "llvm" as const : backend }), - ...(backend === "helper" ? { nativeProgramObject: true } : {}), + outPath: join(outDir, `program-llvm-${optimization}${sanitize ? "-san" : ""}`), + outDir, sanitize, dynamic: wantsDynamic(file), optimization, }); } -// Auto-discovery ledger, summarized after the run: which programs the tier -// claims, and which IR kinds gate the rest (phase 2's queue). -// SCRIPTC_LLVM_REFUSALS=1 additionally lists every refused program under -// its kind — the burn-down view the phase reports work from. -const claimed: string[] = []; -const refusalKinds = new Map(); -const refusalPrograms = new Map(); - -describe(`llvm differential corpus (${files.length} programs${sanitize ? ", sanitized" : ""}${shardSuffix()})`, () => { - test.for(files.map((f) => [f.slice(corpusDir.length + 1), f] as const))( - "%s", - async ([rel, file]) => { - const llvmRes = await build(file, "llvm"); - if (!llvmRes.ok) { - // Out of tier: the refusal must be LOUD and must be THE refusal — - // exactly one SC3001 naming the first unhandled construct. Any - // other diagnostic here means a corpus program stopped compiling - // at all, which the main differential suite forbids. - expect(llvmRes.diagnostics.map((d) => d.code)).toEqual(["SC3001"]); - const kind = /\(([^)]+)\)/.exec(llvmRes.diagnostics[0]!.message)?.[1] ?? "?"; - refusalKinds.set(kind, (refusalKinds.get(kind) ?? 0) + 1); - refusalPrograms.set(kind, [...(refusalPrograms.get(kind) ?? []), rel]); - // The release default must land this same program on the C lane - // TRANSPARENTLY: one frontend pass, the emit retried through the - // C backend, the refusal recorded — never a failed build. - const defRes = await build(file, "default"); - if (!defRes.ok) throw new Error(`the default lane failed to fall back on a refused program: ${rel}`); - expect(defRes.backend).toBe("c"); - expect(defRes.llvmRefusal).toBe(kind); - expect(defRes.cPath.endsWith(".c")).toBe(true); - return; - } - claimed.push(rel); - expect(llvmRes.backend).toBe("llvm"); - expect(llvmRes.llvmRefusal).toBeUndefined(); - expect(llvmRes.cPath.endsWith(".ll")).toBe(true); - - if (helperOnly) { - if (sanitize) throw new Error("the helper object lane does not support sanitizer mode"); - if (process.platform !== "darwin" || process.arch !== "arm64" || - Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) < 24) { - throw new Error("the helper object lane requires macOS 15+ arm64"); - } - const helperRes = await build(file, "helper"); - if (!helperRes.ok) { - throw new Error( - `LLVM helper object emission failed on a program the LLVM tier claims: ${rel}: ` + - helperRes.diagnostics.map((d) => `${d.code} ${d.message}`).join("; "), - ); - } - const [llvm, helper] = await Promise.all([ - runBinary(llvmRes.binaryPath, []), - runBinary(helperRes.binaryPath, []), - ]); - expect(helper.stdout, `helper object stdout differed for ${rel}`).toEqual(llvm.stdout); - if (helper.exitCode === 0) { - expect(comparableStderr(helper.stderr), `helper object stderr differed for ${rel}`) - .toEqual(comparableStderr(llvm.stderr)); - } - expect(helper.exitCode, `helper object exit code differed for ${rel}`).toBe(llvm.exitCode); - return; - } - - const cRes = await build(file, "c"); - if (!cRes.ok) throw new Error(`C backend failed on a program the LLVM tier claims: ${rel}`); - expect(cRes.backend).toBe("c"); - const [llvm, c, node] = await Promise.all([ - runBinary(llvmRes.binaryPath, []), - runBinary(cRes.binaryPath, []), - runBinary("node", nodeOracleArgs(file)), - ]); - - // stdout: byte parity across all three lanes. - if (!llvm.stdout.equals(c.stdout)) { - expect(llvm.stdout.toString("utf8")).toBe(c.stdout.toString("utf8")); - expect.unreachable("llvm-vs-c stdout differed at byte level but not after utf8 decode"); - } - if (!llvm.stdout.equals(node.stdout)) { - expect(llvm.stdout.toString("utf8")).toBe(node.stdout.toString("utf8")); - expect.unreachable("llvm-vs-node stdout differed at byte level but not after utf8 decode"); - } - // stderr: the exit-0 contract of the main differential suite. - const expectedExit = expectedExitCode(file); - if (expectedExit === 0) { - const llvmErr = comparableStderr(llvm.stderr); - const cErr = comparableStderr(c.stderr); - if (!llvmErr.equals(cErr)) { - expect(llvmErr.toString("utf8")).toBe(cErr.toString("utf8")); - } - if (!llvmErr.equals(node.stderr)) { - expect(llvmErr.toString("utf8")).toBe(node.stderr.toString("utf8")); - } - } - expect(llvm.exitCode).toBe(expectedExit); - expect(c.exitCode).toBe(expectedExit); - expect(node.exitCode).toBe(expectedExit); - }, - ); - - test("tier floor: the survey's six programs stay claimed", () => { - // Under a shard, only the floor programs THIS slice ran can be asserted - // (same key as the corpus split above); the shard union covers all six. - for (const name of shardSelect(TIER_FLOOR, (n) => n)) { - expect(claimed, `${name} regressed out of the LLVM tier`).toContain(name); - } - }); - - test("fixed tier regressions stay claimed", () => { - for (const name of shardSelect(TIER_REGRESSIONS, (n) => n)) { - expect(claimed, `${name} regressed out of the LLVM tier`).toContain(name); - } - }); - - test.skipIf(!helperOnly)("helper object feature floor stays claimed", () => { - for (const name of shardSelect(HELPER_TIER_FLOOR, (n) => n)) { - expect(claimed, `${name} regressed out of helper object emission`).toContain(name); - } - }); - - afterAll(() => { - const hist = [...refusalKinds].sort((a, b) => b[1] - a[1]); - // eslint-disable-next-line no-console - console.info( - `llvm tier: ${claimed.length}/${files.length} corpus programs claimed; ` + - `top refusals: ${hist.slice(0, 8).map(([k, n]) => `${k}×${n}`).join(", ")}`, - ); - if (process.env["SCRIPTC_LLVM_REFUSALS"] === "1") { - for (const [kind] of hist) { - // eslint-disable-next-line no-console - console.info(` ${kind}: ${refusalPrograms.get(kind)!.join(" ")}`); - } +describe(`llvm differential corpus (${files.length} programs, ${optimizationModes.join("+")}${sanitize ? ", sanitized" : ""}${shardSuffix()})`, () => { + test.for(files.map((f) => [f.slice(corpusDir.length + 1), f] as const))("%s", async ([rel, file]) => { + const oracle = await runBinary(process.execPath, nodeOracleArgs(file)); + const expectedExit = expectedExitCode(file); + expect(oracle.exitCode).toBe(expectedExit); + for (const optimization of optimizationModes) { + const result = await build(file, optimization); + if (!result.ok) throw new Error(`${rel} (${optimization}): ` + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; ")); + expect(result.backend).toBe("llvm"); + expect(result.llvmPath.endsWith(".ll")).toBe(true); + const actual = await runBinary(result.binaryPath, []); + expect(actual.stdout, `${optimization} stdout`).toEqual(oracle.stdout); + if (expectedExit === 0) expect(comparableStderr(actual.stderr), `${optimization} stderr`).toEqual(oracle.stderr); + expect(actual.exitCode, `${optimization} exit status`).toBe(expectedExit); } }); }); diff --git a/tests/harness/native-object-example.test.ts b/tests/harness/native-object-example.test.ts index a46bac651..6dd1f9d2d 100644 --- a/tests/harness/native-object-example.test.ts +++ b/tests/harness/native-object-example.test.ts @@ -41,7 +41,7 @@ describe.runIf(supported)("external native object example", () => { package: "@scriptc/runtime", version: "1.0.0", root: runtimeRoot, - source_sets: [], + objects: [], archives: [], }, program: { object: join(scratch, "missing.o") }, ffi: { libraries: [] }, diff --git a/tests/harness/native-proxy.test.ts b/tests/harness/native-proxy.test.ts index fba13db1d..805ac14b3 100644 --- a/tests/harness/native-proxy.test.ts +++ b/tests/harness/native-proxy.test.ts @@ -8,7 +8,7 @@ import { compile } from "@scriptc/compiler"; const execFileAsync = promisify(execFile); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native Proxy boundaries unwind catchably (${backend})`, async () => { const outDir = mkdtempSync(join(tmpdir(), "scriptc-proxy-boundaries-")); const entry = join(outDir, "main.mjs"); diff --git a/tests/harness/native-weak-collections.test.ts b/tests/harness/native-weak-collections.test.ts index 875b68571..f5b08c042 100644 --- a/tests/harness/native-weak-collections.test.ts +++ b/tests/harness/native-weak-collections.test.ts @@ -8,7 +8,7 @@ import { compile } from "@scriptc/compiler"; const exec = promisify(execFile); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`weak collections keep engine identity in dynamic builds (${backend})`, async () => { const outDir = await mkdtemp(join(tmpdir(), "scriptc-weak-island-")); try { diff --git a/tests/harness/node-test.test.ts b/tests/harness/node-test.test.ts index 9273b363a..672226d43 100644 --- a/tests/harness/node-test.test.ts +++ b/tests/harness/node-test.test.ts @@ -90,7 +90,7 @@ async function build(entry: string): Promise { // Pinned: the spec-reporter normalizations here were written against // the C lane; lane identity stays fixed so a diff means the reporter // (or Node) changed, never that the default backend moved. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( diff --git a/tests/harness/npm-fetch-defaults.test.ts b/tests/harness/npm-fetch-defaults.test.ts index f22971216..7445618cc 100644 --- a/tests/harness/npm-fetch-defaults.test.ts +++ b/tests/harness/npm-fetch-defaults.test.ts @@ -6,7 +6,7 @@ import { compile } from "@scriptc/compiler"; import { expect, test } from "vitest"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`embedded package destructuring defaults retain global fetch (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-fetch-defaults-")); try { diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 0e1ce26fa..fa333b90e 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -87,7 +87,7 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise // Pinned: the suite pins --npm-static's FRONTEND frontier (coverage // numbers, fence sites); the backend lane is held fixed so those pins // move only when the frontend moves. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( @@ -99,7 +99,7 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise } describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { - test.each(["c", "llvm"] as const)("renderer values and callbacks compile from shipped JavaScript (%s)", async (backend) => { + test.each(["llvm"] as const)("renderer values and callbacks compile from shipped JavaScript (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-renderer-values-")); try { const pkg = join(dir, "node_modules", "renderer-values"); @@ -130,7 +130,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { test.each(["stored-native-builtins.js", "stored-object-helpers.js", "renderer-specializations.js", "renderer-export-dictionaries.js", "fresh-array-union-layout.js", "async-backend-factory.mjs", "destructure-inferred-row-defaults.js", "object-from-checked-entries.js", "regex-checked-storage.js", "set-checked-methods.js", "frozen-checked-dictionaries.mjs", "checked-renderer-defaults.js"].flatMap((name) => - (["c", "llvm"] as const).map((backend) => ({ name, backend })), + (["llvm"] as const).map((backend) => ({ name, backend })), ))("renderer startup $name compiles from shipped JavaScript ($backend)", async ({ name, backend }) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-renderer-startup-")); try { @@ -157,7 +157,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { } }); - test.each(["c", "llvm"] as const)("bundled class aliases preserve declared methods and callback fields (%s)", async (backend) => { + test.each(["llvm"] as const)("bundled class aliases preserve declared methods and callback fields (%s)", async (backend) => { const entry = join(pilotRoot, "bundled-methods-cli.ts"); const { coverage } = analyze(entry, { npmStatic: "auto" }); expect(coverage.npmStatic).toEqual([{ package: "bundled-methods", status: "static" }]); @@ -193,7 +193,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { } }); - test.each(["c", "llvm"] as const)("untyped package methods preserve virtual overrides (%s)", async (backend) => { + test.each(["llvm"] as const)("untyped package methods preserve virtual overrides (%s)", async (backend) => { const entry = join(pilotRoot, "virtual-classes-cli.ts"); const { coverage } = analyze(entry, { npmStatic: "auto" }); expect(coverage.npmStatic).toEqual([{ package: "virtual-classes", status: "static" }]); @@ -208,7 +208,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }); - test.each(["c", "llvm"] as const)("a package's literal createRequire calls compile without an engine (%s)", async (backend) => { + test.each(["llvm"] as const)("a package's literal createRequire calls compile without an engine (%s)", async (backend) => { const entry = join(pilotRoot, "module-loader-cli.ts"); const { coverage } = analyze(entry, { npmStatic: "auto" }); expect(coverage.npmStatic).toEqual([{ package: "module-loader", status: "static" }]); @@ -273,7 +273,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { } }); - test.each(["c", "llvm"] as const)("bundled CommonJS function imports compile without an engine (%s)", async (backend) => { + test.each(["llvm"] as const)("bundled CommonJS function imports compile without an engine (%s)", async (backend) => { const entry = join(pilotRoot, "bundled-function-cli.ts"); const { coverage } = analyze(entry, { npmStatic: "auto" }); expect(coverage.npmStatic).toEqual([{ package: "bundled-function", status: "static" }]); diff --git a/tests/harness/opaque-class-view.test.ts b/tests/harness/opaque-class-view.test.ts index 6f602dbe6..066b6d9e4 100644 --- a/tests/harness/opaque-class-view.test.ts +++ b/tests/harness/opaque-class-view.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { compile } from "@scriptc/compiler"; import { expect, test } from "vitest"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`opaque class fields refuse dynamic views while preserving exact recovery (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-opaque-class-")); try { diff --git a/tests/harness/opentui-native.mts b/tests/harness/opentui-native.mts index cf21c3bee..bd9abaff9 100644 --- a/tests/harness/opentui-native.mts +++ b/tests/harness/opentui-native.mts @@ -28,7 +28,7 @@ writeFileSync(profile, JSON.stringify({ { name: "nativeRender", symbol: "probe_render", params: ["string", "mutable-bytes"], returns: "u32" }, ], }, null, 2)); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { const result = await compile(join(fixtures, "native.ts"), { backend, dynamic: false, outDir: join(out, backend), outPath: join(out, `opentui-${backend}`), ffiProfilePath: profile, }); diff --git a/tests/harness/portless-e2e.test.ts b/tests/harness/portless-e2e.test.ts index f035908f3..770f5651c 100644 --- a/tests/harness/portless-e2e.test.ts +++ b/tests/harness/portless-e2e.test.ts @@ -111,7 +111,7 @@ suite("Portless acceptance", () => { const built = await compile(entry, { outDir: work, outPath: binary, - backend: "c", + backend: "llvm", sanitize: process.env["SCRIPTC_SAN"] === "1", }); expect(built.ok).toBe(true); diff --git a/tests/harness/process-argv.test.ts b/tests/harness/process-argv.test.ts index cf01de1a1..bb7a8ab5f 100644 --- a/tests/harness/process-argv.test.ts +++ b/tests/harness/process-argv.test.ts @@ -9,7 +9,7 @@ const root = join(import.meta.dirname, "../.."); const entry = join(root, "tests/fixtures/npm/cases/process-argv.mts"); const args = ["", "héllo", "中文", "🌍", "a b", 'double"quote', "end\\", 'slashes\\\\"quote', "&;$(hello)`test'", "line\nbreak", "tab\targ"]; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`island process.argv preserves Unicode, quoting, empty values and identity (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-argv-island-")); try { diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index eae1a156b..3234b21e8 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -18,9 +18,7 @@ import { analyze, compile, renderDiagnostics } from "@scriptc/compiler"; /* Every compile below deliberately carries NO backend pin: this suite is * the user-adoption path, so it must see exactly what a flagless - * `scriptc build` produces — the LLVM backend where the tier claims the - * program, the transparent C fallback where it refuses. A failure here - * that a `backend: "c"` pin would hide is a release-default bug. */ + * `scriptc build` produces through the production LLVM backend. */ const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); const fixture = (name: string) => join(repoRoot, "tests/fixtures/strictness", name); diff --git a/tests/harness/provenance.test.ts b/tests/harness/provenance.test.ts index 0bb5293d2..031e3367d 100644 --- a/tests/harness/provenance.test.ts +++ b/tests/harness/provenance.test.ts @@ -41,7 +41,7 @@ async function buildAndRun(name: string, dynamic: boolean, program = entry): Pro // Pinned: the provenance thesis compares a source-static binary against // a dist-island binary — holding both on the C lane keeps that byte // comparison about PROVENANCE, never about which backend each build drew. - const result = await compile(program, { outPath, outDir, dynamic, backend: "c" }); + const result = await compile(program, { outPath, outDir, dynamic, backend: "llvm" }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); } diff --git a/tests/harness/regex.test.ts b/tests/harness/regex.test.ts index 9e8910bb0..fdcdf3d3f 100644 --- a/tests/harness/regex.test.ts +++ b/tests/harness/regex.test.ts @@ -27,7 +27,7 @@ const platformTest = process.env["SCRIPTC_PORTABLE_ONLY"] === "1" ? test.skip : interface BuildResult { binaryPath: string; - cPath: string; + llvmPath: string; } /** Compiles an inline program (static by default — regex needs no flag). */ @@ -46,17 +46,14 @@ async function build( mkdirSync(outDir, { recursive: true }); const file = join(outDir, `${name}.ts`); writeFileSync(file, source); - // Pinned: the size/stability pins below grep the emitted C (no ScrRegex - // symbol) and weigh the C-lane binary — this suite measures the C - // backend's artifact by design. - const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize: san, backend: "c" }); + const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize: san, backend: "llvm" }); if (!result.ok) { throw new Error( "regex program failed to compile:\n" + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), ); } - return { binaryPath: result.binaryPath, cPath: result.cPath }; + return { binaryPath: result.binaryPath, llvmPath: result.llvmPath }; } describe("regex (scriptc-only behavior)", () => { @@ -122,9 +119,8 @@ console.log(/${"(a)".repeat(300)}/.test("a")); { sanitize: false }, ), ]); - const plainC = readFileSync(plainBuild.cPath, "utf8"); - expect(plainC).not.toContain("ScrRegex"); - expect(plainC).not.toContain("scr_regex"); + const plainC = readFileSync(plainBuild.llvmPath, "utf8"); + expect(plainC).not.toContain("@scr_regex"); // The class bounds are page-granular (macOS rounds segments to 16KB, // so a few hundred bytes of new runtime can tip a whole page): the net // loop hooks, the console/process/child surface (the piped-stream diff --git a/tests/harness/runtime-resolver.test.ts b/tests/harness/runtime-resolver.test.ts index 446c3eb7f..78c8529f6 100644 --- a/tests/harness/runtime-resolver.test.ts +++ b/tests/harness/runtime-resolver.test.ts @@ -169,7 +169,7 @@ test("runtime lookup tracks failed candidates as well as selected files", () => } finally { rmSync(directory, { recursive: true, force: true }); } }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native runtime resolver: ${backend} matches pinned Node without evaluating modules`, async () => { const directory = workspace(); try { diff --git a/tests/harness/runtime-tree-shaking.test.ts b/tests/harness/runtime-tree-shaking.test.ts index d991fd98a..1af62f354 100644 --- a/tests/harness/runtime-tree-shaking.test.ts +++ b/tests/harness/runtime-tree-shaking.test.ts @@ -15,12 +15,7 @@ import { compile } from "@scriptc/compiler"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/runtime-tree-shaking"); -// These source-toolchain contracts are safe in the Linux Sandboxes used by -// `test:sandbox`: they deliberately use the C backend and `nm`, both of -// which are part of that image. Keep the Windows host out of this POSIX -// fixture (its child program uses /bin/echo), but do not use -// SCRIPTC_PORTABLE_ONLY here: that marker means "run in a Linux Sandbox", -// not "skip native executable assertions". +// The executable and symbol contracts use POSIX tools and /bin/echo. const sourceToolchainTest = process.platform === "win32" ? test.skip : test; interface Fixture { @@ -96,7 +91,7 @@ async function build(name: string, source: string) { outDir, // The C lane proves source-toolchain linking. macOS additionally runs // the default lane below, which selects the helper/runtime-pack path. - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); diff --git a/tests/harness/sandbox-config.test.ts b/tests/harness/sandbox-config.test.ts index 71f0b4a7c..d2c8df5cd 100644 --- a/tests/harness/sandbox-config.test.ts +++ b/tests/harness/sandbox-config.test.ts @@ -5,6 +5,7 @@ import { sandboxBootstrapCommand, sandboxImageConfig, sandboxRunnerConfig, + sandboxTestSourceConfig, sandboxTestWorkerAllocation, sandboxVcrConfig, sandboxVercelConfig, @@ -94,6 +95,25 @@ test("custom images must be fully qualified VCR references", () => { ); }); +test("prepared snapshots select the snapshot CLI source without changing project scope", () => { + const config = sandboxTestSourceConfig({ SCRIPTC_SANDBOX_SNAPSHOT: " snap_ABC123 " }); + expect(config).toEqual({ + prepared: true, reference: "snap_ABC123", description: "prepared snapshot", createArgs: ["--snapshot", "snap_ABC123"], + }); + expect(sandboxBootstrapCommand(config.prepared)).toBeUndefined(); +}); + +test("test sources retain managed and custom image preparation", () => { + expect(sandboxTestSourceConfig({})).toMatchObject({ prepared: false, createArgs: ["--image", defaultSandboxImage] }); + const image = "vcr.vercel.com/team/project/tests:node24"; + expect(sandboxTestSourceConfig({ SCRIPTC_SANDBOX_IMAGE: image })).toMatchObject({ prepared: true, createArgs: ["--image", image] }); +}); + +test("ambiguous or malformed snapshot sources fail before creating workers", () => { + expect(() => sandboxTestSourceConfig({ SCRIPTC_SANDBOX_SNAPSHOT: "snap_ABC123", SCRIPTC_SANDBOX_IMAGE: "image" })).toThrow("Set only one"); + expect(() => sandboxTestSourceConfig({ SCRIPTC_SANDBOX_SNAPSHOT: "wrong-id" })).toThrow("snapshot ID"); +}); + test("OIDC authentication wins and supplies its own Sandbox scope", () => { const config = sandboxVercelConfig({ VERCEL_OIDC_TOKEN: "header.payload.signature", diff --git a/tests/harness/sandbox-dockerfile.test.ts b/tests/harness/sandbox-dockerfile.test.ts deleted file mode 100644 index 298087410..000000000 --- a/tests/harness/sandbox-dockerfile.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, test } from "vitest"; - -const dockerfile = readFileSync(new URL("../../Dockerfile.sandbox", import.meta.url), "utf8"); -const dockerignore = readFileSync(new URL("../../.dockerignore", import.meta.url), "utf8"); - -describe("Sandbox Dockerfile", () => { - test("copies CLI lifecycle scripts before installing workspace dependencies", () => { - const packageIndex = dockerfile.indexOf( - "COPY packages/cli/package.json packages/cli/package.json", - ); - const scriptIndex = dockerfile.indexOf( - "COPY packages/cli/scripts/warm-cache.mjs packages/cli/scripts/warm-cache.mjs", - ); - const installIndex = dockerfile.indexOf("RUN pnpm install --frozen-lockfile"); - - expect(packageIndex).toBeGreaterThanOrEqual(0); - expect(scriptIndex).toBeGreaterThan(packageIndex); - expect(installIndex).toBeGreaterThan(scriptIndex); - expect(dockerignore).toContain("!packages/cli/scripts/"); - expect(dockerignore).toContain("!packages/cli/scripts/warm-cache.mjs"); - }); -}); diff --git a/tests/harness/self-hosting-backend-analysis.test.ts b/tests/harness/self-hosting-backend-analysis.test.ts index eb28e76ec..a0be70347 100644 --- a/tests/harness/self-hosting-backend-analysis.test.ts +++ b/tests/harness/self-hosting-backend-analysis.test.ts @@ -5,8 +5,7 @@ import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { expect, test } from "vitest"; -import { compile, compileC, deserializeModule, emitCModule, serializeModule, validateModule, type AnalyzeResult } from "@scriptc/compiler"; -import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js"; +import { compile, compileC, deserializeModule, emitLlvmModule, serializeModule, validateModule, type AnalyzeResult } from "@scriptc/compiler"; import { everyStmtList } from "../../packages/compiler/src/ir/traverse.js"; import { moduleUsesInspect, moduleUsesDynInvoke, moduleUsesRegex, moduleUsesCopying, type IrModule } from "../../packages/compiler/src/ir/ir.js"; import { backendAnalysisCases } from "./self-hosting-backend-cases.js"; @@ -58,7 +57,7 @@ test("the production optimization and backend analysis pipeline lowers entirely expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`self-hosting backend analysis and optimization (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-analysis-")); const sanitize = process.env["SCRIPTC_SAN"] === "1"; @@ -124,8 +123,8 @@ for (const backend of ["c", "llvm"] as const) { // Feed the native result to each production backend. This pins // optimizer semantics beyond matching Node's JSON: the resulting // native executable must agree with the original TypeScript. - const path = join(dir, backend === "c" ? "optimized.c" : "optimized.ll"); - writeFileSync(path, backend === "c" ? emitCModule(optimized) : emitLlvmModule(optimized)); + const path = join(dir, "optimized.ll"); + writeFileSync(path, emitLlvmModule(optimized)); const outPath = executable("program"); await compileC({ cPath: path, outPath, sanitize, diff --git a/tests/harness/self-hosting-bootstrap.test.ts b/tests/harness/self-hosting-bootstrap.test.ts index a83893376..e79c4a54c 100644 --- a/tests/harness/self-hosting-bootstrap.test.ts +++ b/tests/harness/self-hosting-bootstrap.test.ts @@ -3,11 +3,12 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { isDeepStrictEqual, promisify } from "node:util"; +import { promisify } from "node:util"; import { expect, test } from "vitest"; -import { compileC, deserializeModule, emitCModule, validateModule } from "@scriptc/compiler"; +import { compileC, deserializeModule, validateModule } from "@scriptc/compiler"; import type { compile } from "@scriptc/compiler"; -import * as ir from "../../packages/compiler/src/ir/ir.js"; +import { nativeFeatures } from "./self-hosting-native-features.js"; +import { bootstrapStep } from "./self-hosting-timing.js"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; const root = join(import.meta.dirname, "../.."); @@ -15,27 +16,17 @@ const execFileAsync = promisify(execFile); const sanitize = process.env["SCRIPTC_SAN"] === "1"; const options = { cwd: root, timeout: 1_200_000, maxBuffer: 16 * 1024 * 1024 }; -function nativeFeatures(module: ir.IrModule) { - return { - regex: ir.moduleUsesRegex(module), copying: ir.moduleUsesCopying(module), - inspect: ir.moduleUsesInspect(module), dynInvoke: ir.moduleUsesDynInvoke(module), - symbol: ir.moduleUsesSymbol(module), bigint: ir.moduleUsesBigInt(module), - zlib: ir.moduleUsesZlib(module), assert: ir.moduleUsesAssert(module), - textDecoderLegacy: ir.moduleUsesLegacyTextDecoder(module), fileHandle: ir.moduleUsesFileHandle(module), - fetch: ir.moduleUsesFetch(module), dc: ir.moduleUsesDc(module), dynAsync: ir.moduleUsesDynAsync(module), - events: ir.moduleUsesProcessEvents(module), emitter: ir.moduleUsesEmitter(module), - searchParams: ir.moduleUsesSearchParams(module), qs: ir.moduleUsesQs(module), parseArgs: ir.moduleUsesParseArgs(module), - stream: ir.moduleUsesStream(module), net: ir.moduleUsesNet(module), http: ir.moduleUsesHttpServer(module), - http2: ir.moduleUsesHttp2(module), dgram: ir.moduleUsesDgram(module), watch: ir.moduleUsesFsWatch(module), - nodeTest: ir.moduleUsesNodeTest(module), tls: ir.moduleUsesTls(module), tlsCa: ir.moduleUsesTlsCa(module), - }; +function comparableStderr(text: string): string { + return sanitize + ? text.replace(/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, "") + : text; } -test("the native frontend and C emitter rebuild a working frontend from its TypeScript source", async () => { +test("the native frontend and LLVM emitter rebuild a working frontend from its TypeScript source", async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-bootstrap-")); const executable = (name: string) => join(directory, name + (process.platform === "win32" ? ".exe" : "")); const frontend = join(root, "tests/fixtures/self-hosting/frontend-lowering.ts"); - const emitter = join(root, "tests/fixtures/self-hosting/c-emitter.ts"); + const emitter = join(root, "tests/fixtures/self-hosting/llvm-emitter.ts"); const nativeOptions = { ...options, env: { ...process.env, PATH: "" } }; try { const nativeSources = join(root, "packages/compiler/native"); @@ -50,11 +41,11 @@ test("the native frontend and C emitter rebuild a working frontend from its Type const buildSeed = async (entry: string, name: string, ffi?: string) => { const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href; const built = await execFileAsync(process.execPath, [ - "--import", "tsx", "--input-type=module", "--eval", + "--max-old-space-size=8192", "--import", "tsx", "--input-type=module", "--eval", `import { compile } from ${JSON.stringify(api)}; const result = await compile(process.argv[1], { - outDir: process.argv[2], outPath: process.argv[3], backend: 'c', - dynamic: false, optimization: 'dev', sanitize: process.argv[4] === '1', + outDir: process.argv[2], outPath: process.argv[3], backend: 'llvm', + dynamic: false, optimization: 'release', strip: true, sanitize: process.argv[4] === '1', ffiProfilePath: process.argv[5] || undefined, emitIr: true, }); console.log(JSON.stringify({ ...result, sourceTexts: undefined }));`, @@ -64,33 +55,50 @@ test("the native frontend and C emitter rebuild a working frontend from its Type const result = JSON.parse(built.stdout) as Awaited>; if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); if (!("binaryPath" in result)) throw new Error("expected a native frontend executable"); - expect(result.backend).toBe("c"); + expect(result.backend).toBe("llvm"); expect(result.irPath).toBeDefined(); return result; }; - const seed = await buildSeed(frontend, "frontend-seed", profile); - const nativeEmitter = await buildSeed(emitter, "emitter"); + const seed = await bootstrapStep("build frontend seed", () => buildSeed(frontend, "frontend-seed", profile)); + const nativeEmitter = await bootstrapStep("build LLVM emitter seed", () => buildSeed(emitter, "emitter")); + const emitterRequest = join(directory, "emitter-request.json"); + writeFileSync(emitterRequest, JSON.stringify({ debug: true, sources: [], pointerBits: 64, wasi: false, emitLibraryIdentity: true, runtimeAbiMarker: false })); const ownIr = join(directory, "frontend-native.json"); - const self = await execFileAsync(seed.binaryPath, [ts7Executable(), frontend, ownIr, profile], nativeOptions); + const self = await bootstrapStep("frontend lowers itself", () => + execFileAsync(seed.binaryPath, [ts7Executable(), frontend, ownIr, profile], nativeOptions)); expect(self.stdout).toBe("0\n0 0\n"); - expect(self.stderr).toBe(""); + expect(comparableStderr(self.stderr)).toBe(""); // Compare complete IR, including layouts and helper signatures. A valid // module alone can hide different optional-return inference or dropped // method-registry mutations in the native compiler. - const module = deserializeModule(readFileSync(ownIr, "utf8")); - expect(module.functions.length).toBeGreaterThan(1_000); - expect(validateModule(module)).toEqual([]); - expect(isDeepStrictEqual(module, deserializeModule(readFileSync(seed.irPath!, "utf8"))), "native self-lowering must match the Node seed").toBe(true); - - const cPath = join(directory, "frontend-native.c"); - const emitted = await execFileAsync(nativeEmitter.binaryPath, [ownIr, cPath], nativeOptions); + const cPath = join(directory, "frontend-native.ll"); + const emitted = await bootstrapStep("emit frontend LLVM", () => + execFileAsync(nativeEmitter.binaryPath, [ownIr, cPath, emitterRequest], nativeOptions)); expect(emitted.stdout).toBe(""); - expect(emitted.stderr).toBe(""); - // Avoid rendering hundreds of megabytes in an assertion failure. - expect(readFileSync(cPath, "utf8") === emitCModule(module, undefined, { debugSources: new Map() }), "native C emission must match Node byte-for-byte").toBe(true); + expect(comparableStderr(emitted.stderr)).toBe(""); + // Full compiler graphs exceed the worker's default heap. Keep both the + // structural IR comparison and reference emission in a roomy child. + const verification = await bootstrapStep("compare frontend IR and LLVM", () => execFileAsync(process.execPath, [ + "--max-old-space-size=8192", "--import", "tsx", "--input-type=module", "--eval", + `import assert from 'node:assert/strict'; + import { readFileSync } from 'node:fs'; + import { isDeepStrictEqual } from 'node:util'; + import { deserializeModule, validateModule, emitLlvmModule } from ${JSON.stringify(pathToFileURL(join(root, "packages/compiler/src/index.ts")).href)}; + import { nativeFeatures } from ${JSON.stringify(new URL("./self-hosting-native-features.ts", import.meta.url).href)}; + const module = deserializeModule(readFileSync(process.argv[1], 'utf8')); + assert.ok(module.functions.length > 1000); + assert.deepEqual(validateModule(module), []); + assert.ok(isDeepStrictEqual(module, deserializeModule(readFileSync(process.argv[2], 'utf8'))), 'native self-lowering must match the Node seed'); + assert.ok(readFileSync(process.argv[3], 'utf8') === emitLlvmModule(module, { debugSources: new Map() }), 'native LLVM emission must match Node exactly'); + console.log(JSON.stringify(nativeFeatures(module)));`, + ownIr, seed.irPath!, cPath, + ], options)); + expect(verification.stderr).toBe(""); + const features = JSON.parse(verification.stdout) as ReturnType; const rebuilt = executable("frontend-rebuilt"); - await compileC({ cPath, outPath: rebuilt, optimization: "dev", sanitize, linkInputs: [object], ...nativeFeatures(module) }); + await bootstrapStep("build second frontend generation", () => + compileC({ cPath, outPath: rebuilt, optimization: "release", strip: true, sanitize, linkInputs: [object], ...features })); // The rebuilt compiler consumes new source, and its emitted program // executes. These inputs exercise the two bugs found by self-compiling: @@ -105,14 +113,14 @@ test("the native frontend and C emitter rebuild a working frontend from its Type expect(expected.stdout).toBe("0\n0 0\n"); expect(actual.stdout).toBe(expected.stdout); expect(expected.stderr).toBe(""); - expect(actual.stderr).toBe(""); + expect(comparableStderr(actual.stderr)).toBe(""); const program = deserializeModule(readFileSync(actualIr, "utf8")); expect(program).toEqual(deserializeModule(readFileSync(expectedIr, "utf8"))); expect(validateModule(program)).toEqual([]); - const programC = join(directory, "program.c"); - const emission = await execFileAsync(nativeEmitter.binaryPath, [actualIr, programC], nativeOptions); + const programC = join(directory, "program.ll"); + const emission = await execFileAsync(nativeEmitter.binaryPath, [actualIr, programC, emitterRequest], nativeOptions); expect(emission.stdout).toBe(""); - expect(emission.stderr).toBe(""); + expect(comparableStderr(emission.stderr)).toBe(""); await compileC({ cPath: programC, outPath: executable("program"), sanitize, ...nativeFeatures(program) }); const oracle = spawnSync(process.execPath, [source], options); const native = spawnSync(executable("program"), [], options); diff --git a/tests/harness/self-hosting-c-embedding.test.ts b/tests/harness/self-hosting-c-embedding.test.ts deleted file mode 100644 index 63f24ecd4..000000000 --- a/tests/harness/self-hosting-c-embedding.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { deflateRawSync } from "node:zlib"; -import { expect, test } from "vitest"; -import { cStringLiteral } from "../../packages/compiler/src/backend/c/types.js"; -import { normalizedEmbeddingC } from "./self-hosting-c-embedding.js"; -import { cEmitterCases } from "./self-hosting-c-emitter-cases.js"; -import { emitCModule } from "@scriptc/compiler"; - -const item = cEmitterCases().find((item) => item.name === "level-nine module and facade compression")!; -const original = emitCModule(item.module); -const declaration = /(static const char sc_npm_src_0\[\] = [^\n]*\n)((?:\s*"(?:[^"\\]|\\.)*"\s*)+);/; - -function withSource(bytes: Buffer): string { - return original.replace(declaration, (_match, prefix: string) => `${prefix} ${cStringLiteral(bytes)};`); -} - -test("different valid DEFLATE encodings compare by their full source bytes", () => { - const source = item.module.embedded!.modules[0]!.source; - const stored = deflateRawSync(source, { level: 0 }); - const compressed = deflateRawSync(source, { level: 9 }); - expect(stored.equals(compressed)).toBe(false); - expect(normalizedEmbeddingC(withSource(stored), item.module)).toBe(normalizedEmbeddingC(withSource(compressed), item.module)); -}); - -test("embedding comparison rejects a corrupted compressed payload", () => { - expect(() => normalizedEmbeddingC(withSource(Buffer.from([255, 255, 255])), item.module)).toThrow(); -}); - -test("embedding comparison rejects a valid stream containing the wrong source", () => { - const wrong = deflateRawSync("a different module", { level: 9 }); - expect(() => normalizedEmbeddingC(withSource(wrong), item.module)).toThrow("embedded source differs"); -}); - -test("embedding comparison checks the raw byte length including UTF-8", () => { - const wrong = original.replace(/(sc_npm_src_0, sizeof sc_npm_src_0 - 1, )\d+/, "$11"); - expect(() => normalizedEmbeddingC(wrong, item.module)).toThrow("incorrect raw length"); -}); - -test("embedding comparison checks every facade as well as the module", () => { - const changed = { ...item.module, embedded: { - ...item.module.embedded!, - modules: [{ ...item.module.embedded!.modules[0]!, esm: "export default 'wrong';" }], - } }; - expect(() => normalizedEmbeddingC(original, changed)).toThrow("embedded source differs for sc_npm_esm_0"); -}); - -test("normalization preserves runtime setup and edge condition metadata", () => { - const actual = normalizedEmbeddingC(original, item.module); - expect(actual).toContain("scr_zlib_inflate_exact"); - expect(actual).toContain("sc_npm_edges"); - expect(actual).toContain('"/app/main.js", "large", "/node_modules/large/index.js", 2'); - const changed = original.replace("scr_zlib_inflate_exact", "incorrect_inflater"); - expect(normalizedEmbeddingC(changed, item.module)).not.toBe(actual); -}); diff --git a/tests/harness/self-hosting-c-embedding.ts b/tests/harness/self-hosting-c-embedding.ts deleted file mode 100644 index 797970162..000000000 --- a/tests/harness/self-hosting-c-embedding.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { inflateRawSync } from "node:zlib"; -import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; - -/** Decode the C backend's byte-string literal grammar. This intentionally - * accepts only its emitted forms, so a malformed escape or lost chunk fails - * the native-emitter comparison instead of being normalized away. */ -function literalBytes(literals: string): Buffer { - const bytes: number[] = []; - let offset = 0; - while (offset < literals.length) { - if (/\s/.test(literals[offset]!)) { offset++; continue; } - if (literals[offset++] !== '"') throw new Error("expected C string chunk"); - let closed = false; - while (offset < literals.length) { - const ch = literals[offset++]!; - if (ch === '"') { closed = true; break; } - if (ch !== "\\") { - const code = ch.charCodeAt(0); - if (code < 32 || code > 126) throw new Error("non-ASCII C literal byte"); - bytes.push(code); - continue; - } - const escape = literals[offset++]!; - if (/[0-7]/.test(escape)) { - const octal = escape + literals.slice(offset, offset + 2); - if (!/^[0-7]{3}$/.test(octal)) throw new Error("invalid C octal byte"); - const byte = parseInt(octal, 8); - if (byte > 255) throw new Error("C octal escape exceeds one byte"); - bytes.push(byte); - offset += 2; - } else if (escape === '"' || escape === "\\" || escape === "?") { - bytes.push(escape.charCodeAt(0)); - } else { - throw new Error(`unexpected C escape ${escape}`); - } - } - if (!closed) throw new Error("unterminated C string chunk"); - } - return Buffer.from(bytes); -} - -/** zlib implementations can choose different valid DEFLATE streams. Verify - * every byte of each inflated source/facade and its raw-length metadata, - * then normalize only the encoded literal for the C text comparison. All - * declarations, table rows, edge conditions and runtime setup remain exact. */ -export function normalizedEmbeddingC(text: string, mod: IrModule): string { - let normalized = text; - const check = (name: string, source: string): void => { - const declaration = new RegExp(`(static const char ${name}\\[\\] = [^\\n]*\\n)((?:\\s*"(?:[^"\\\\]|\\\\.)*"\\s*)+);`); - const match = declaration.exec(normalized); - if (!match) throw new Error(`missing embedded declaration ${name}`); - const metadata = new RegExp(`\\b${name}, sizeof ${name} - 1, (\\d+)`).exec(normalized); - if (!metadata) throw new Error(`missing raw-length metadata for ${name}`); - const rawLength = Number(metadata[1]); - const encoded = literalBytes(match[2]!); - const bytes = rawLength === 0 ? encoded : inflateRawSync(encoded); - const expected = Buffer.from(source, "utf8"); - if (!bytes.equals(expected)) throw new Error(`embedded source differs for ${name}`); - if (rawLength !== 0 && rawLength !== bytes.length) throw new Error(`incorrect raw length for ${name}`); - const canonical = `"${bytes.toString("base64")}"`; - normalized = normalized.replace(declaration, (_all, prefix: string) => `${prefix} ${canonical};`); - }; - for (const [index, module] of (mod.embedded?.modules ?? []).entries()) { - check(`sc_npm_src_${index}`, module.source); - if (module.esm !== undefined) check(`sc_npm_esm_${index}`, module.esm); - } - return normalized; -} diff --git a/tests/harness/self-hosting-c-emitter.test.ts b/tests/harness/self-hosting-c-emitter.test.ts deleted file mode 100644 index 72499cf92..000000000 --- a/tests/harness/self-hosting-c-emitter.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { execFile, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { promisify } from "node:util"; -import { expect, test } from "vitest"; -import { compile, compileC, deserializeModule, emitCModule, serializeModule, validateModule } from "@scriptc/compiler"; -import { - moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect, - moduleUsesRegex, moduleUsesSymbol, moduleUsesZlib, type IrModule, -} from "../../packages/compiler/src/ir/ir.js"; -import { cEmitterCases } from "./self-hosting-c-emitter-cases.js"; -import { normalizedEmbeddingC } from "./self-hosting-c-embedding.js"; - -const root = fileURLToPath(new URL("../..", import.meta.url)); -const entry = join(root, "tests/fixtures/self-hosting/c-emitter.ts"); -const execFileAsync = promisify(execFile); -const runOptions = { cwd: root, timeout: 60_000, maxBuffer: 128 * 1024 * 1024 }; -const sanitize = process.env["SCRIPTC_SAN"] === "1"; - -function programStderr(stderr: Buffer): string { - const text = stderr.toString("utf8"); - return sanitize - ? text.replace(/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, "") - : text; -} - -function nativeFeatures(mod: IrModule) { - return { - regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod), - inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod), - symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), zlib: moduleUsesZlib(mod), - assert: moduleUsesAssert(mod), - }; -} - -// This list exercises generated code, not only helper text. It spans the -// emitter's statement, expression, ownership, async, class and dynamic-value -// paths while keeping bootstrap validation a focused test of this stage. -const programs = [ - "001-hello.ts", - "101-arithmetic.ts", - "600-closures-basic.ts", - "711-inheritance-dispatch.ts", - "756-cycle-inheritance.ts", - "803-switch-rc-stress.ts", - "1005-json-nested.ts", - "1023-async-rc-stress.ts", - "1401-typedarray-slice-set.ts", - "1452-return-through-finally.ts", - "1672-symbol-containers.ts", - "2012-generators-return-throw.ts", - "2537-destructuring-assign-member-targets.ts", - "2840-finally-completions.ts", - "2905-bigint-edges.ts", - "3103-scalar-record-nested-control-flow.ts", - "3104-native-analysis-tables-loops.ts", - "3109-identity-union-collections.ts", - "3112-union-switch-control-flow.ts", - "3113-union-array-removal.ts", - "3114-union-record-field-write.ts", - "3115-zlib-static-compression-levels.ts", - "3116-sequence-expression-lifetimes.ts", - "3117-unknown-switch-control-flow.ts", - "3118-recursive-union-spread.ts", -]; - -for (const backend of ["c", "llvm"] as const) { - test(`the complete C emitter runs natively (${backend})`, async () => { - const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-c-emitter-")); - const executable = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : "")); - const stage = executable("emitter"); - try { - // The frontend can spend longer than Vitest's worker-RPC deadline in - // synchronous lowering. Run the same source API in an awaited child. - // Compile without dynamic fallback and keep its IR for the fixed point. - const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href; - const { stdout } = await execFileAsync(process.execPath, [ - "--import", "tsx", "--input-type=module", "--eval", - `import { compile } from ${JSON.stringify(api)}; - const result = await compile(process.argv[1], { - outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4], - dynamic: false, optimization: 'dev', sanitize: process.argv[5] === '1', emitIr: true, - }); - console.log(JSON.stringify(result));`, - entry, dir, stage, backend, sanitize ? "1" : "0", - ], { ...runOptions, timeout: 600_000 }); - const built = JSON.parse(stdout) as Awaited>; - if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - if (!("binaryPath" in built)) throw new Error("bootstrap did not produce an executable"); - expect(built.backend).toBe(backend); - expect(built.llvmRefusal).toBeUndefined(); - expect(built.irPath).toBeDefined(); - const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8")); - expect(ownIr.functions.length).toBeGreaterThan(500); - expect(validateModule(ownIr)).toEqual([]); - - const emit = async (mod: IrModule, name: string, sources: { file: string; text: string }[] = []): Promise => { - const input = join(dir, "input.json"); - const output = join(dir, "native.c"); - const sourcePath = join(dir, "sources.json"); - writeFileSync(input, serializeModule(mod)); - writeFileSync(sourcePath, JSON.stringify(sources)); - const result = await execFileAsync(stage, [input, output, sourcePath], { ...runOptions, timeout: 300_000 }) - .catch((cause: unknown) => { throw new Error(`native C emission failed for ${name}`, { cause }); }); - expect(result.stdout, name).toBe(""); - expect(result.stderr, name).toBe(""); - const text = readFileSync(output, "utf8"); - const expected = emitCModule(mod, undefined, { debugSources: new Map(sources.map((s) => [s.file, s.text])) }); - expect(normalizedEmbeddingC(text, mod), name).toBe(normalizedEmbeddingC(expected, mod)); - return text; - }; - - for (const item of cEmitterCases()) { - expect(validateModule(item.module), item.name).toEqual([]); - const text = await emit(item.module, item.name, item.sources); - for (const fragment of item.contains) expect(text, item.name).toContain(fragment); - for (const fragment of item.excludes ?? []) expect(text, item.name).not.toContain(fragment); - } - - for (const source of programs) { - const sourcePath = join(root, "tests/corpus", source); - const irPath = join(dir, "program.ir.json"); - const lowered = await compile(sourcePath, { outDir: dir, outPath: irPath, outputKind: "ir", dynamic: false }); - if (!lowered.ok) throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`); - const mod = deserializeModule(readFileSync(irPath, "utf8")); - const text = await emit(mod, source, [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }]); - const cPath = join(dir, "program.c"); - const outPath = executable("program"); - writeFileSync(cPath, text); - await compileC({ cPath, outPath, sanitize, optimization: "dev", ...nativeFeatures(mod) }); - const node = spawnSync(process.execPath, [sourcePath], runOptions); - const native = spawnSync(outPath, [], runOptions); - for (const result of [node, native]) { - expect(result.error, source).toBeUndefined(); - expect(result.signal, `${source}: ${result.stderr}`).toBeNull(); - expect(result.status, `${source}: ${result.stderr}`).toBe(0); - } - expect(native.stdout, source).toEqual(node.stdout); - expect(programStderr(native.stderr), source).toBe(programStderr(node.stderr)); - } - - // Bootstrap the backend itself: the native stage emits its own IR, - // then the resulting executable emits that IR again. Exact C equality - // checks the second generation without depending on linker timestamps. - const selfC = await emit(ownIr, "emitter emits itself"); - const selfPath = join(dir, "self.c"); - const secondStage = executable("emitter-second"); - writeFileSync(selfPath, selfC); - await compileC({ cPath: selfPath, outPath: secondStage, sanitize, optimization: "dev", ...nativeFeatures(ownIr) }); - const again = join(dir, "again.c"); - const second = await execFileAsync(secondStage, [join(dir, "input.json"), again, join(dir, "sources.json")], { ...runOptions, timeout: 300_000 }); - expect(second.stdout).toBe(""); - expect(second.stderr).toBe(""); - expect(readFileSync(again, "utf8")).toBe(selfC); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }, 900_000); -} diff --git a/tests/harness/self-hosting-commonjs.test.ts b/tests/harness/self-hosting-commonjs.test.ts index 6fa09c3dd..0cb8b71b4 100644 --- a/tests/harness/self-hosting-commonjs.test.ts +++ b/tests/harness/self-hosting-commonjs.test.ts @@ -43,7 +43,7 @@ function input() { }; } -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`CommonJS detection and package rewrites run without Node (${backend})`, async () => { const directory = mkdtempSync(join(tempRoot, "scriptc-commonjs-native-")); try { diff --git a/tests/harness/self-hosting-compiler-process.ts b/tests/harness/self-hosting-compiler-process.ts new file mode 100644 index 000000000..d0665c4d1 --- /dev/null +++ b/tests/harness/self-hosting-compiler-process.ts @@ -0,0 +1,37 @@ +import { execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import type { AnalyzeOptions, CompileOptions, CompileExecutableResult, CoverageInput } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const root = fileURLToPath(new URL("../../", import.meta.url)); +const api = new URL("../../packages/compiler/src/index.ts", import.meta.url).href; + +// Large synchronous frontend passes must leave Vitest's worker RPC responsive. +// Execute the source API in a child and return only JSON-compatible results. +async function compilerProcess(operation: "analyze" | "compile", entry: string, options: object): Promise { + const { stdout, stderr } = await execFileAsync(process.execPath, [ + "--max-old-space-size=8192", "--import", "tsx", "--input-type=module", "--eval", + `import { analyze, compile } from ${JSON.stringify(api)}; + const options = JSON.parse(process.argv[3]); + const result = process.argv[1] === 'analyze' + ? analyze(process.argv[2], options).coverage + : await compile(process.argv[2], options); + console.log(JSON.stringify(result, (key, value) => key === 'sourceTexts' ? undefined : value));`, + operation, entry, JSON.stringify(options), + ], { cwd: root, timeout: 300_000, maxBuffer: 16 * 1024 * 1024 }); + if (stderr !== "") throw new Error(stderr); + return JSON.parse(stdout); +} + +export async function analyzeInChild(entry: string, options: AnalyzeOptions): Promise { + return await compilerProcess("analyze", entry, options) as CoverageInput; +} + +type ExecutableResult = + | Extract + | Omit, "sourceTexts">; + +export async function compileInChild(entry: string, options: CompileOptions): Promise { + return await compilerProcess("compile", entry, options) as ExecutableResult; +} diff --git a/tests/harness/self-hosting-emission.test.ts b/tests/harness/self-hosting-emission.test.ts index 255b00372..eb1c3deb7 100644 --- a/tests/harness/self-hosting-emission.test.ts +++ b/tests/harness/self-hosting-emission.test.ts @@ -42,7 +42,7 @@ test("the production layout and metadata stage lowers statically", () => { expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native LLVM layout and metadata emission (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-emission-")); const sanitize = process.env["SCRIPTC_SAN"] === "1"; diff --git a/tests/harness/self-hosting-c-emitter-cases.ts b/tests/harness/self-hosting-emitter-inputs.ts similarity index 78% rename from tests/harness/self-hosting-c-emitter-cases.ts rename to tests/harness/self-hosting-emitter-inputs.ts index 6a5eb0428..103a830cb 100644 --- a/tests/harness/self-hosting-c-emitter-cases.ts +++ b/tests/harness/self-hosting-emitter-inputs.ts @@ -5,12 +5,10 @@ import { import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/build.js"; import { IR_VERSION } from "../../packages/compiler/src/ir/serialize.js"; -export interface CEmitterCase { +export interface EmitterInputCase { name: string; module: IrModule; sources: { file: string; text: string }[]; - contains: string[]; - excludes?: string[]; } const loc = { file: "native emitter/日本.ts", start: 0, end: 1 }; @@ -42,14 +40,14 @@ function method(cls: string, member: string): IrFunction { * TypeScript sources cannot request independently (unused layouts, exact * library ABI metadata, module compression and literal interning). The * execution cases in the harness separately check emitted program behavior. */ -export function cEmitterCases(): CEmitterCase[] { - const cases: CEmitterCase[] = []; - const add = (name: string, mod: IrModule, contains: string[], sources: CEmitterCase["sources"] = []): CEmitterCase => { - const item = { name, module: mod, contains, sources }; +export function emitterInputCases(): EmitterInputCase[] { + const cases: EmitterInputCase[] = []; + const add = (name: string, mod: IrModule, sources: EmitterInputCase["sources"] = []): EmitterInputCase => { + const item = { name, module: mod, sources }; cases.push(item); return item; }; - add("empty executable", module(), ["int main(", "scr_runtime.h"]); + add("empty executable", module()); const strings = module(); strings.functions[0]!.body = [ @@ -59,17 +57,17 @@ export function cEmitterCases(): CEmitterCase[] { effect(strLit("repeat", loc)), effect(strLit("repeat", loc)), ]; - add("literal escaping and interning", strings, ["sc_lit_", "ScrStr", "scr_str_retain"]); + add("literal escaping and interning", strings); const numbers = module(); numbers.functions[0]!.body = [-0, 0, 1, -1, Number.MAX_VALUE, Number.MIN_VALUE, Infinity, -Infinity] .map((value) => effect(numLit(value, loc))); numbers.functions[0]!.body.push(effect({ kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc })); - add("floating point constants", numbers, ["INFINITY", " / ", "-0.0"]); + add("floating point constants", numbers); const source = module(); source.functions[0]!.body = [effect(strLit("line two", { ...loc, start: 12, end: 20 }))]; - add("source locations with multibyte text", source, ["#line 2", "native emitter"], [ + add("source locations with multibyte text", source, [ { file: loc.file, text: "// 日本\nconsole.log('line two');\n" }, ]); @@ -82,13 +80,13 @@ export function cEmitterCases(): CEmitterCase[] { ] }, { id: "tuple", tuple: true, fields: [{ name: "0", type: STRING }, { name: "1", type: F64 }] }, ]; - add("scalar, reference and tuple layouts", records, ["scr_str_release", "scr_arr_release", "record tuple"]); + add("scalar, reference and tuple layouts", records); const recursive = module(); recursive.records = [ { id: "node", fields: [{ name: "children", type: arrayOf(record("node")) }, { name: "text", type: STRING }] }, ]; - add("recursive record trace and teardown", recursive, ["scr_cyc_alloc", "visit(o->sc_fld_children, ctx)", "scr_str_release"]); + add("recursive record trace and teardown", recursive); const mutual = module(); mutual.records = [ @@ -99,11 +97,11 @@ export function cEmitterCases(): CEmitterCase[] { { id: "rightMaybe", arms: [record("right"), UNDEFINED_T] }, { id: "leftMaybe", arms: [record("left"), UNDEFINED_T] }, ]; - add("mutually recursive optional records", mutual, ["visit(o->sc_fld_next, ctx)", "scr_union_release", "scr_cyc_free"]); + add("mutually recursive optional records", mutual); const overflow = module(); overflow.records = [{ id: "dictionary", fields: [{ name: "label", type: STRING }], indexValue: record("dictionary") }]; - add("recursive overflow maps", overflow, ["sc_ovf", "visit(o->sc_ovf, ctx)", "scr_map_release"]); + add("recursive overflow maps", overflow); const cls = (name: string, base?: string): IrClassDef => ({ name, ...(base === undefined ? {} : { base }), @@ -112,7 +110,7 @@ export function cEmitterCases(): CEmitterCase[] { const classes = module(); classes.classes = [cls("Root"), cls("Left", "Root"), cls("Right", "Root"), cls("Leaf", "Left")]; for (const c of classes.classes) classes.functions.push(method(c.name, "visit")); - add("class forest and virtual dispatch tables", classes, ["sc_vtable_", "sc_vm_", "sc_vtt_", "class Root", "class Leaf"]); + add("class forest and virtual dispatch tables", classes); const abstract = module(); abstract.classes = [ @@ -120,7 +118,7 @@ export function cEmitterCases(): CEmitterCase[] { cls("Concrete", "Abstract"), ]; abstract.functions.push(method("Concrete", "visit")); - add("abstract method ABI from descendant", abstract, ["class Abstract", "class Concrete", "sc_vm_"]); + add("abstract method ABI from descendant", abstract); const globals = module(); globals.globals = [ @@ -131,7 +129,7 @@ export function cEmitterCases(): CEmitterCase[] { { kind: "assign", localId: "%g.text", value: strLit("global", loc), loc }, { kind: "assign", localId: "%g.values", value: { kind: "arrayLit", elems: [numLit(1, loc)], type: arrayOf(F64), loc }, loc }, ]; - add("global initialization and shutdown ownership", globals, ["scr_str_release", "scr_arr_release", "global"]); + add("global initialization and shutdown ownership", globals); const constantTable = module(); constantTable.globals = [{ id: "%g.table", name: "table", type: arrayOf(F64), mutable: false }]; @@ -139,7 +137,7 @@ export function cEmitterCases(): CEmitterCase[] { { kind: "assign", localId: "%g.table", value: { kind: "arrayLit", elems: [1, 4, 9, 16].map((n) => numLit(n, loc)), type: arrayOf(F64), loc }, loc }, effect({ kind: "arrayGet", arr: varRef("%g.table", arrayOf(F64), loc), index: numLit(2, loc), type: F64, loc }), ]; - add("constant numeric array storage", constantTable, ["static", "scr_arr_push_f64"]); + add("constant numeric array storage", constantTable); const labels = module(); labels.functions[0]!.body = [{ @@ -150,7 +148,7 @@ export function cEmitterCases(): CEmitterCase[] { ], loc, }], loc, }]; - add("switch and block jump labels", labels, ["goto sc_end_", "sc_swcase_"]); + add("switch and block jump labels", labels); const closure = module(); const closureType: IrType = { kind: "func", params: [], ret: STRING }; @@ -164,7 +162,7 @@ export function cEmitterCases(): CEmitterCase[] { { kind: "varDecl", localId: "message", init: strLit("capture", loc), loc }, effect({ kind: "closure", fnName: "reader", captures: ["message"], type: closureType, loc }), ]; - add("closure capture boxes", closure, ["scr_box_get_ref", "scr_box_retain", "scr_closure_new"]); + add("closure capture boxes", closure); const unions = module(); unions.unions = [{ id: "scalar", arms: [BOOL, F64, STRING, UNDEFINED_T] }]; @@ -172,12 +170,12 @@ export function cEmitterCases(): CEmitterCase[] { effect({ kind: "unionWrap", unionId: "scalar", tag: 2, value: strLit("payload", loc), type: union("scalar"), loc }), effect({ kind: "unionWrap", unionId: "scalar", tag: 3, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: union("scalar"), loc }), ]; - add("union scalar payloads and immortal units", unions, ["scr_union_new_ref", "sc_unit_", "SIZE_MAX"]); + add("union scalar payloads and immortal units", unions); const dictionary = module(); const mapType = mapOf(STRING, F64); dictionary.functions[0]!.body = [effect({ kind: "mapNew", type: mapType, loc })]; - add("typed map construction", dictionary, ["scr_map_new"]); + add("typed map construction", dictionary); const shortNpm = module(); shortNpm.embedded = { @@ -191,7 +189,7 @@ export function cEmitterCases(): CEmitterCase[] { { from: "/node_modules/tiny/index.js", specifier: "node:path", to: "node:path", kind: "any" }, ], }; - add("uncompressed npm, JSON and CJS facade tables", shortNpm, ["sc_npm_src_0", "sc_npm_esm_2", "sc_npm_edges", "export const n"]); + add("uncompressed npm, JSON and CJS facade tables", shortNpm); const compressed = module(); compressed.embedded = { @@ -201,7 +199,7 @@ export function cEmitterCases(): CEmitterCase[] { { from: "/app/main.js", specifier: "large", to: "/node_modules/large/index.js", kind: "import" }, ], }; - add("level-nine module and facade compression", compressed, ["sc_npm_src_0", "sc_npm_esm_0", "scr_zlib_inflate_exact"]); + add("level-nine module and facade compression", compressed); const chunked = module(); // A deterministic byte distribution prevents this source from being a @@ -213,7 +211,7 @@ export function cEmitterCases(): CEmitterCase[] { text += String.fromCharCode(32 + (state % 95)); } chunked.embedded = { modules: [{ key: "/node_modules/random/data.json", format: "json", source: JSON.stringify(text) }], edges: [] }; - add("large source literal chunk boundaries", chunked, ["sc_npm_src_0", "ScrIslandModule"]); + add("large source literal chunk boundaries", chunked); const library = module(); library.lib = { @@ -222,12 +220,12 @@ export function cEmitterCases(): CEmitterCase[] { threadInstances: false, exports: [], trapOverlays: [], identity: { buildIdSymbol: "native_build_id", abiVersionSymbol: "native_abi", buildId: "0123456789abcdef", abiVersion: 7 }, }; - add("library entry points and identity constants", library, ["native_init", "native_collect", "native_reset", "native_build_id", "0123456789abcdef"]).excludes = ["int main("]; + add("library entry points and identity constants", library); const threaded = module(); threaded.lib = { ...library.lib, threadInstances: true }; threaded.globals = [{ id: "%g.counter", name: "counter", type: F64, mutable: true }]; threaded.functions[0]!.body = [{ kind: "assign", localId: "%g.counter", value: numLit(0, loc), loc }]; - add("thread-local library globals", threaded, ["_Thread_local", "native_init"]); + add("thread-local library globals", threaded); return cases; } diff --git a/tests/harness/self-hosting-ffi-profile.test.ts b/tests/harness/self-hosting-ffi-profile.test.ts index a242cee1c..3c6f37d28 100644 --- a/tests/harness/self-hosting-ffi-profile.test.ts +++ b/tests/harness/self-hosting-ffi-profile.test.ts @@ -26,7 +26,7 @@ test("the production FFI profile parser lowers entirely statically", () => { expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native FFI profile validation and release resolution (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-ffi-profile-")); try { diff --git a/tests/harness/self-hosting-frontend-collections.test.ts b/tests/harness/self-hosting-frontend-collections.test.ts index f22ecdc27..e4cfed414 100644 --- a/tests/harness/self-hosting-frontend-collections.test.ts +++ b/tests/harness/self-hosting-frontend-collections.test.ts @@ -9,7 +9,7 @@ const root = join(import.meta.dirname, "../.."); const entry = join(root, "tests/fixtures/self-hosting/frontend-collections.ts"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`frontend builtin and package registries execute natively (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-frontend-collections-")); try { diff --git a/tests/harness/self-hosting-frontend-metadata.test.ts b/tests/harness/self-hosting-frontend-metadata.test.ts index 7a7b00273..2ca630bd4 100644 --- a/tests/harness/self-hosting-frontend-metadata.test.ts +++ b/tests/harness/self-hosting-frontend-metadata.test.ts @@ -8,7 +8,7 @@ import { expect, test } from "vitest"; const root = join(import.meta.dirname, "../.."); const entry = join(root, "tests/fixtures/self-hosting/frontend-metadata.ts"); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`production package transforms and TS7 options execute natively (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-frontend-metadata-")); try { diff --git a/tests/harness/self-hosting-frontend-program.test.ts b/tests/harness/self-hosting-frontend-program.test.ts index dd1b2d96e..7c496cfef 100644 --- a/tests/harness/self-hosting-frontend-program.test.ts +++ b/tests/harness/self-hosting-frontend-program.test.ts @@ -1,8 +1,9 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { expect, test } from "vitest"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; import type { FrontendProgramRequest } from "../fixtures/self-hosting/frontend-program-cases.js"; @@ -13,6 +14,7 @@ const oracle = join(root, "tests/fixtures/self-hosting/frontend-program-node.ts" const nativeSources = join(root, "packages/compiler/native"); const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const execFileAsync = promisify(execFile); function inputs(directory: string): FrontendProgramRequest { const cases: FrontendProgramRequest["cases"] = []; @@ -222,7 +224,7 @@ function verify(reports: Report[]): void { expect(get("npm-restored").order).toEqual(get("npm-static").order); } -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`production program loading and preflight run without Node (${backend})`, async () => { const directory = mkdtempSync(join(tempRoot, "scriptc-frontend-program-")); try { @@ -236,30 +238,25 @@ for (const backend of ["c", "llvm"] as const) { const request = join(directory, "request.json"); const expected = join(directory, "node.json"); writeFileSync(request, JSON.stringify(inputs(directory))); - const node = spawnSync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 90_000 }); - expect(node.error, node.stderr).toBeUndefined(); - expect(node.status, node.stderr).toBe(0); + const node = await execFileAsync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 90_000 }); expect(node.stdout).toBe(""); expect(node.stderr).toBe(""); const nodeReports = JSON.parse(readFileSync(expected, "utf8")) as Report[]; verify(nodeReports); // Restore edits made by the Node run before starting the native client. writeFileSync(request, JSON.stringify(inputs(directory))); - const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile }); + const coverage = await analyzeInChild(entry, { dynamic: false, ffiProfilePath: profile }); expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false); expect(coverage.stats.statementsFailed, JSON.stringify(coverage.diagnostics)).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); - const built = await compile(entry, { + const built = await compileInChild(entry, { backend, dynamic: false, optimization: "dev", sanitize, ffiProfilePath: profile, outDir: directory, outPath: join(directory, process.platform === "win32" ? "frontend.exe" : "frontend"), }); if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const report = join(directory, "native.json"); - const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 90_000 }); - expect(run.error, run.stderr).toBeUndefined(); - expect(run.signal, run.stderr).toBeNull(); - expect(run.status, run.stderr).toBe(0); + const run = await execFileAsync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 90_000 }); expect(run.stdout).toBe(node.stdout); expect(run.stderr).toBe(node.stderr); const nativeReports = JSON.parse(readFileSync(report, "utf8")) as Report[]; diff --git a/tests/harness/self-hosting-frontend-services.test.ts b/tests/harness/self-hosting-frontend-services.test.ts index c12a7c6b3..bcd81e07c 100644 --- a/tests/harness/self-hosting-frontend-services.test.ts +++ b/tests/harness/self-hosting-frontend-services.test.ts @@ -37,7 +37,7 @@ function input(directory: string) { }; } -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`owned frontend services run without Node (${backend})`, async () => { const directory = mkdtempSync(join(tempRoot, "scriptc-frontend-services-native-")); try { diff --git a/tests/harness/self-hosting-integer-ranges.test.ts b/tests/harness/self-hosting-integer-ranges.test.ts index 221264559..06f5565bd 100644 --- a/tests/harness/self-hosting-integer-ranges.test.ts +++ b/tests/harness/self-hosting-integer-ranges.test.ts @@ -23,7 +23,7 @@ test("production integer-range analysis lowers statically with identity-keyed ma expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native integer-range analysis preserves proofs and expression identity (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-ranges-")); try { diff --git a/tests/harness/self-hosting-llvm-embedding.test.ts b/tests/harness/self-hosting-llvm-embedding.test.ts index 779264083..2ca0f342a 100644 --- a/tests/harness/self-hosting-llvm-embedding.test.ts +++ b/tests/harness/self-hosting-llvm-embedding.test.ts @@ -2,10 +2,10 @@ import { deflateRawSync } from "node:zlib"; import { expect, test } from "vitest"; import { llvmBytes } from "../../packages/compiler/src/backend/literals.js"; import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js"; -import { cEmitterCases } from "./self-hosting-c-emitter-cases.js"; +import { emitterInputCases } from "./self-hosting-emitter-inputs.js"; import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js"; -const item = cEmitterCases().find((item) => item.name === "level-nine module and facade compression")!; +const item = emitterInputCases().find((item) => item.name === "level-nine module and facade compression")!; const original = emitLlvmModule(item.module); function withSource(bytes: Buffer): string { diff --git a/tests/harness/self-hosting-llvm-emitter-cases.ts b/tests/harness/self-hosting-llvm-emitter-cases.ts index c8d5f4867..602f1539e 100644 --- a/tests/harness/self-hosting-llvm-emitter-cases.ts +++ b/tests/harness/self-hosting-llvm-emitter-cases.ts @@ -1,7 +1,7 @@ import { F64, type IrModule } from "../../packages/compiler/src/ir/ir.js"; import { numLit } from "../../packages/compiler/src/ir/build.js"; import type { LlvmTargetOptions } from "../../packages/compiler/src/backend/llvm/emitter.js"; -import { cEmitterCases } from "./self-hosting-c-emitter-cases.js"; +import { emitterInputCases } from "./self-hosting-emitter-inputs.js"; export interface LlvmEmitterRequest { targetTriple?: string; @@ -37,9 +37,9 @@ export interface LlvmEmitterCase { } export function llvmEmitterCases(): LlvmEmitterCase[] { - // Both complete backends consume the same IR, including library entry + // The LLVM emitter consumes complete IR, including library entry // points, recursive layouts, closure boxes and embedded package tables. - const cases: LlvmEmitterCase[] = cEmitterCases().map((item) => ({ + const cases: LlvmEmitterCase[] = emitterInputCases().map((item) => ({ name: item.name, module: item.module, request: llvmEmitterRequest({ debug: item.sources.length > 0, sources: item.sources }), contains: item.module.lib ? ["@native_init", "@native_collect"] : ["define i32 @main("], diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts index 3438dbf87..0dfc3bc82 100644 --- a/tests/harness/self-hosting-llvm-emitter.test.ts +++ b/tests/harness/self-hosting-llvm-emitter.test.ts @@ -8,8 +8,8 @@ import { expect, test } from "vitest"; import { compile, compileC, deserializeModule, serializeModule, validateModule } from "@scriptc/compiler"; import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js"; import { - moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect, - moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSymbol, moduleUsesZlib, type IrModule, + moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesEmitter, moduleUsesInspect, + moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesStream, moduleUsesSymbol, moduleUsesZlib, type IrModule, } from "../../packages/compiler/src/ir/ir.js"; import { llvmEmitterCases, llvmEmitterOptions, llvmEmitterRequest, type LlvmEmitterRequest } from "./self-hosting-llvm-emitter-cases.js"; import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js"; @@ -33,6 +33,7 @@ function nativeFeatures(mod: IrModule) { inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod), symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), zlib: moduleUsesZlib(mod), assert: moduleUsesAssert(mod), + emitter: moduleUsesEmitter(mod), stream: moduleUsesStream(mod), textDecoderLegacy: moduleUsesLegacyTextDecoder(mod), }; } @@ -69,10 +70,11 @@ const programs = [ "3118-recursive-union-spread.ts", "3119-runtime-optional-spread.ts", "3120-ir-nonfinite-numbers.ts", + "4031-event-emitter-long-tuples.ts", "text-codec-values/main.ts", ]; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`the complete LLVM emitter bootstraps natively (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-llvm-emitter-")); const executable = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : "")); @@ -98,7 +100,7 @@ for (const backend of ["c", "llvm"] as const) { if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); if (!("binaryPath" in built)) throw new Error("bootstrap did not produce an executable"); expect(built.backend).toBe(backend); - expect(built.llvmRefusal).toBeUndefined(); + expect(built.irPath).toBeDefined(); const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8")); expect(ownIr.functions.length).toBeGreaterThan(800); diff --git a/tests/harness/self-hosting-lowering-context.test.ts b/tests/harness/self-hosting-lowering-context.test.ts index 6211fca76..d6c272c31 100644 --- a/tests/harness/self-hosting-lowering-context.test.ts +++ b/tests/harness/self-hosting-lowering-context.test.ts @@ -15,7 +15,7 @@ const nativeSources = join(root, "packages/compiler/native"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; const execFileAsync = promisify(execFile); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`production lexical contexts run with native TS7 symbols (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-lowering-context-")); try { @@ -73,7 +73,7 @@ for (const backend of ["c", "llvm"] as const) { expect(coverage.stats.functionsSkipped).toBe(0); if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); expect(built.backend).toBe(backend); - expect(built.llvmRefusal).toBeUndefined(); + const output = join(directory, "native.json"); const run = spawnSync(built.binaryPath, [ts7Executable(), source, output], { encoding: "utf8", timeout: 90_000 }); expect(run.error, run.stderr).toBeUndefined(); diff --git a/tests/harness/self-hosting-native-driver.test.ts b/tests/harness/self-hosting-native-driver.test.ts index 09f3121ad..43b1aac0f 100644 --- a/tests/harness/self-hosting-native-driver.test.ts +++ b/tests/harness/self-hosting-native-driver.test.ts @@ -5,9 +5,17 @@ import { delimiter, join, resolve } from "node:path"; import { promisify } from "node:util"; import { expect, test } from "vitest"; import type { NativeToolchainManifest } from "../../packages/compiler/src/native/toolchain.js"; +import { bootstrapStep } from "./self-hosting-timing.js"; const root = join(import.meta.dirname, "../.."); const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +function comparableStderr(text: string): string { + return sanitize + ? text.replace(/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, "") + : text; +} function absoluteCommand(command: string): string { if (command.includes("/") || command.includes("\\")) return resolve(command); @@ -26,12 +34,18 @@ test("the standalone compiler builds programs and rebuilds itself with Node unav const executable = (name: string) => join(directory, name + (process.platform === "win32" ? ".exe" : "")); const options = { cwd: root, timeout: 1_800_000, maxBuffer: 16 * 1024 * 1024 }; try { - await exec(process.execPath, ["--max-old-space-size=8192", "--import", "tsx", join(root, "scripts/build-native-compiler.mts"), directory], options); + await bootstrapStep("build native compiler seed", () => + exec(process.execPath, ["--max-old-space-size=8192", "--import", "tsx", join(root, "scripts/build-native-compiler.mts"), directory], options)); const seed = executable("scriptc-native"); const manifestPath = seed + ".json"; const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NativeToolchainManifest; - manifest.c_compiler = absoluteCommand(manifest.c_compiler); manifest.linker = absoluteCommand(manifest.linker); + if (process.platform === "linux") { + // Clang delegates linking to a separate executable. Resolve it before + // removing PATH so the complete native toolchain remains available. + const linker = await exec(manifest.linker, [...manifest.linker_args, "--print-prog-name=ld"], options); + manifest.linker_args.push("--ld-path=" + absoluteCommand(linker.stdout.trim())); + } if (process.platform === "darwin") manifest.dsymutil = absoluteCommand(manifest.dsymutil); writeFileSync(manifestPath, JSON.stringify(manifest)); const nativeOptions = { ...options, env: { ...process.env, PATH: "" } }; @@ -40,8 +54,8 @@ test("the standalone compiler builds programs and rebuilds itself with Node unav const failure = error as Error & { code?: string | number; signal?: string; stdout?: string; stderr?: string }; throw new Error(`${failure.message}\ncode=${failure.code} signal=${failure.signal}\n${failure.stderr ?? ""}\n${failure.stdout ?? ""}`, { cause: error }); }); - expect(result.stderr).toBe(""); - const built = JSON.parse(result.stdout) as { outputPath: string; stats: { + expect(comparableStderr(result.stderr)).toBe(""); + const built = JSON.parse(result.stdout) as { outputPath: string; llvmPath?: string; stats: { statementsTotal: number; statementsFailed: number; statementsIsland: number; functionsSkipped: number; } }; expect(built.stats.statementsFailed).toBe(0); @@ -66,8 +80,11 @@ test("the standalone compiler builds programs and rebuilds itself with Node unav expect(actual.stderr).toEqual(oracle.stderr); }; const sample = join(root, "tests/corpus/class-array-optional-return.ts"); - await checkProgram(seed, sample, "c"); + const unionSample = join(root, "tests/corpus/union-nested-layout-discriminant.ts"); + const receiverSample = join(root, "tests/corpus/llvm-read-receiver-lifetime.ts"); await checkProgram(seed, sample, "llvm"); + await checkProgram(seed, unionSample, "llvm"); + await checkProgram(seed, receiverSample, "llvm"); const badSource = join(directory, "bad.ts"); writeFileSync(badSource, 'const value: number = "wrong"; console.log(value);\n'); @@ -81,18 +98,25 @@ test("the standalone compiler builds programs and rebuilds itself with Node unav const entry = join(root, "packages/compiler/src/native/main.ts"); const profile = join(directory, "ts7-process.ffi.json"); const rebuilt = executable("scriptc-rebuilt"); - const self = await invoke(seed, [entry, "-o", rebuilt, "--backend=c", "--dev", "--strip", "--ffi", profile]); + // Optimize the compiler that will process the full graph again. Small + // programs above and below still exercise development output. + const self = await bootstrapStep("native compiler rebuilds itself", () => + invoke(seed, [entry, "-o", rebuilt, "--backend=llvm", "--strip", "--keep-llvm", "--ffi", profile])); expect(self.stats.statementsTotal).toBeGreaterThan(10_000); + expect(self.llvmPath).toBe(rebuilt + ".ll"); console.log("native self-build", self.stats); - await checkProgram(rebuilt, join(root, "tests/corpus/nullish-long-chain.ts"), "c"); + await checkProgram(rebuilt, join(root, "tests/corpus/nullish-long-chain.ts"), "llvm"); await checkProgram(rebuilt, sample, "llvm"); + await checkProgram(rebuilt, unionSample, "llvm"); + await checkProgram(rebuilt, receiverSample, "llvm"); - // Both generations lower and emit the complete driver. Comparing C - // avoids the >512MB JSON string required by its expanded IR graph. - const seedC = join(directory, "seed.c"); - const rebuiltC = join(directory, "rebuilt.c"); - await invoke(seed, [entry, "--emit=c", "-o", seedC, "--ffi", profile]); - await invoke(rebuilt, [entry, "--emit=c", "-o", rebuiltC, "--ffi", profile]); - expect(readFileSync(seedC).equals(readFileSync(rebuiltC)), "native compiler generations must emit identical C").toBe(true); + // Compare the LLVM used to build the second generation with its own + // output. Retaining the build input avoids repeating the seed's work. + if (!self.llvmPath) throw new Error("native self-build did not retain its LLVM"); + const seedLlvm = self.llvmPath; + const rebuiltLlvm = join(directory, "rebuilt.ll"); + await bootstrapStep("rebuilt compiler emits itself", () => + invoke(rebuilt, [entry, "--emit=llvm", "-o", rebuiltLlvm, "--ffi", profile])); + expect(readFileSync(seedLlvm).equals(readFileSync(rebuiltLlvm)), "native compiler generations must emit identical LLVM").toBe(true); } finally { rmSync(directory, { recursive: true, force: true }); } }, 5_400_000); diff --git a/tests/harness/self-hosting-native-features.ts b/tests/harness/self-hosting-native-features.ts new file mode 100644 index 000000000..a72a5f2f5 --- /dev/null +++ b/tests/harness/self-hosting-native-features.ts @@ -0,0 +1,6 @@ +import { executableLinkFeatures } from "../../packages/compiler/src/backend/executable-features.js"; +import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; + +export function nativeFeatures(module: IrModule) { + return executableLinkFeatures(module, false); +} diff --git a/tests/harness/self-hosting-native-frontend.test.ts b/tests/harness/self-hosting-native-frontend.test.ts index 6ce09456d..031248964 100644 --- a/tests/harness/self-hosting-native-frontend.test.ts +++ b/tests/harness/self-hosting-native-frontend.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { expect, test } from "vitest"; -import { compileC, deserializeModule, emitCModule, validateModule } from "@scriptc/compiler"; +import { compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler"; import type { compile } from "@scriptc/compiler"; import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; @@ -38,7 +38,7 @@ const programs = [ "contextual-array-union-write.ts", ]; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`the complete frontend produces executable IR without Node (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-frontend-")); const executable = (name: string) => join(directory, name + (process.platform === "win32" ? ".exe" : "")); @@ -55,7 +55,7 @@ for (const backend of ["c", "llvm"] as const) { // Build the actual entry in a child while keeping Vitest responsive. const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href; const { stdout, stderr } = await execFileAsync(process.execPath, [ - "--import", "tsx", "--input-type=module", "--eval", + "--max-old-space-size=8192", "--import", "tsx", "--input-type=module", "--eval", `import { compile } from ${JSON.stringify(api)}; const result = await compile(process.argv[1], { outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4], @@ -112,8 +112,8 @@ for (const backend of ["c", "llvm"] as const) { stats: { statementsFailed: 0, statementsIsland: 0, functionsSkipped: 0 }, diagnostics: [], validation: [], }); if (!module) throw new Error(file + ": missing native IR"); - const cPath = join(directory, "program.c"); - writeFileSync(cPath, emitCModule(module)); + const cPath = join(directory, "program.ll"); + writeFileSync(cPath, emitLlvmModule(module)); await compileC({ cPath, outPath: executable("program"), sanitize, regex: moduleUsesRegex(module), copying: moduleUsesCopying(module), @@ -209,8 +209,8 @@ for (const backend of ["c", "llvm"] as const) { const helperObject = join(directory, "add.o"); writeFileSync(helper, "double native_add(double a, double b) { return a + b; }\n"); execFileSync("clang", ["-c", helper, "-o", helperObject]); - const ffiC = join(directory, "ffi.c"); - writeFileSync(ffiC, emitCModule(ffiResult.module)); + const ffiC = join(directory, "ffi.ll"); + writeFileSync(ffiC, emitLlvmModule(ffiResult.module)); await compileC({ cPath: ffiC, outPath: executable("ffi"), sanitize, inspect: true, linkInputs: [helperObject] }); const ffiRun = spawnSync(executable("ffi"), [], runOptions); expect(ffiRun.error).toBeUndefined(); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 85a58f281..e73ba2c1a 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { nativeCodegenTarget } from "../../packages/compiler/src/backend/targets.js"; import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js"; import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js"; @@ -20,20 +20,20 @@ const features: NativeLinkFeatures = { http2: false, dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false, }; -test("native toolchain configuration, staging and helper validation are static", () => { - const { coverage } = analyze(entry, { dynamic: false }); +test("native toolchain configuration, staging and helper validation are static", async () => { + const coverage = await analyzeInChild(entry, { dynamic: false }); expect(coverage.preflightFailed).toBe(false); expect(coverage.stats.statementsFailed).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native toolchain integrity and failure behavior match Node (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-integrity-")); const target = nativeCodegenTarget()!; try { - const built = await compile(entry, { + const built = await compileInChild(entry, { outDir: directory, outPath: join(directory, "toolchain" + target.outputSuffixes.exe), backend, optimization: "dev", dynamic: false, sanitize: process.env["SCRIPTC_SAN"] === "1", }); diff --git a/tests/harness/self-hosting-program-host.test.ts b/tests/harness/self-hosting-program-host.test.ts index 8992bb32b..b577bcc80 100644 --- a/tests/harness/self-hosting-program-host.test.ts +++ b/tests/harness/self-hosting-program-host.test.ts @@ -13,7 +13,7 @@ const oracle = join(root, "tests/fixtures/self-hosting/ts7-program-host-node.ts" const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`the shared program host runs statically with its own TypeScript server (${backend})`, async () => { const dir = mkdtempSync(join(tempRoot, "scriptc-program-host-")); try { diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index 370eaa1cc..ce26b8e24 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -52,13 +52,13 @@ const featureCalls: readonly [string, IrLibFn][] = [ test("the production runtime dependency scanners lower entirely statically", () => { const { coverage } = analyze(entry, { dynamic: false }); expect(coverage.preflightFailed).toBe(false); - expect(coverage.stats.statementsTotal).toBeGreaterThan(500); + expect(coverage.stats.statementsTotal).toBeGreaterThan(150); expect(coverage.stats.statementsFailed).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`self-hosting runtime dependency detection (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-features-")); try { diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 609f983cd..4ac14e0dd 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { NATIVE_TARGETS, nativeCodegenTarget } from "../../packages/compiler/src/backend/targets.js"; import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js"; import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js"; @@ -18,8 +18,8 @@ const base: NativeLinkFeatures = { http2: false, dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false, }; -test("runtime manifest validation and selection are entirely static", () => { - const { coverage } = analyze(entry, { dynamic: false }); +test("runtime manifest validation and selection are entirely static", async () => { + const coverage = await analyzeInChild(entry, { dynamic: false }); expect(coverage.preflightFailed).toBe(false); expect(coverage.stats.statementsTotal).toBeGreaterThan(50); expect(coverage.stats.statementsFailed).toBe(0); @@ -27,11 +27,11 @@ test("runtime manifest validation and selection are entirely static", () => { expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native runtime manifest selection matches Node (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-pack-selection-")); try { - const built = await compile(entry, { + const built = await compileInChild(entry, { outDir: directory, outPath: join(directory, "selection" + (process.platform === "win32" ? ".exe" : "")), backend, dynamic: false, optimization: "dev", sanitize: process.env["SCRIPTC_SAN"] === "1", }); diff --git a/tests/harness/self-hosting-serialization.test.ts b/tests/harness/self-hosting-serialization.test.ts index fce8b1c07..e5716118e 100644 --- a/tests/harness/self-hosting-serialization.test.ts +++ b/tests/harness/self-hosting-serialization.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, test } from "vitest"; -import { analyze, compile, compileC, deserializeModule, emitCModule, serializeModule, validateModule } from "@scriptc/compiler"; +import { analyze, compile, compileC, deserializeModule, emitLlvmModule, serializeModule, validateModule } from "@scriptc/compiler"; import { IR_VERSION } from "../../packages/compiler/src/ir/serialize.js"; import { F64, VOID, type IrModule } from "../../packages/compiler/src/ir/ir.js"; import { numLit } from "../../packages/compiler/src/ir/build.js"; @@ -43,7 +43,7 @@ test("the production IR serialization and validation pipeline lowers entirely st expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`self-hosting serialization: ${backend} round-trips IR and produces a working program`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-serialize-")); const sanitize = process.env["SCRIPTC_SAN"] === "1"; @@ -90,8 +90,8 @@ for (const backend of ["c", "llvm"] as const) { expect(third.kind).toBe("numLit"); if (third.kind !== "numLit") throw new Error("literal changed"); expect(Object.is(third.value, -0)).toBe(true); - const cPath = join(dir, "numbers.c"); - writeFileSync(cPath, emitCModule(numeric)); + const cPath = join(dir, "numbers.ll"); + writeFileSync(cPath, emitLlvmModule(numeric)); await compileC({ cPath, outPath: executable("numbers"), sanitize }); const program = spawnSync(executable("numbers"), [], runOptions); expect(program.error).toBeUndefined(); diff --git a/tests/harness/self-hosting-source-parser.test.ts b/tests/harness/self-hosting-source-parser.test.ts index 887f8b599..33a521b2f 100644 --- a/tests/harness/self-hosting-source-parser.test.ts +++ b/tests/harness/self-hosting-source-parser.test.ts @@ -1,8 +1,9 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { expect, test } from "vitest"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; @@ -12,6 +13,7 @@ const oracle = join(root, "tests/fixtures/self-hosting/ts7-source-parser-node.ts const nativeSources = join(root, "packages/compiler/native"); const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const execFileAsync = promisify(execFile); function input() { const fixtures = join(root, "tests/fixtures/npm-static/node_modules"); @@ -80,7 +82,7 @@ require("cjs");`, kind: "ts" }, }; } -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`source parsing, import scanning and declaration projection run without Node (${backend})`, async () => { const directory = mkdtempSync(join(tempRoot, "scriptc-source-parser-native-")); try { @@ -91,13 +93,13 @@ for (const backend of ["c", "llvm"] as const) { writeFileSync(profile, JSON.stringify({ ...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), libraries: [object], })); - const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile }); + const coverage = await analyzeInChild(entry, { dynamic: false, ffiProfilePath: profile }); expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false); expect(coverage.diagnostics).toEqual([]); expect(coverage.stats.statementsFailed).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); - const built = await compile(entry, { + const built = await compileInChild(entry, { backend, dynamic: false, optimization: "dev", sanitize, ffiProfilePath: profile, outDir: directory, outPath: join(directory, process.platform === "win32" ? "parser.exe" : "parser"), }); @@ -105,16 +107,11 @@ for (const backend of ["c", "llvm"] as const) { const request = join(directory, "request.json"); writeFileSync(request, JSON.stringify(input())); const expected = join(directory, "node.json"); - const node = spawnSync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 45_000 }); - expect(node.error, node.stderr).toBeUndefined(); - expect(node.status, node.stderr).toBe(0); + const node = await execFileAsync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 45_000 }); expect(node.stdout).toBe(""); expect(node.stderr).toBe(""); const report = join(directory, "native.json"); - const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 45_000 }); - expect(run.error, run.stderr).toBeUndefined(); - expect(run.signal, run.stderr).toBeNull(); - expect(run.status, run.stderr).toBe(0); + const run = await execFileAsync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 45_000 }); expect(run.stdout).toBe(node.stdout); expect(run.stderr).toBe(node.stderr); const result = JSON.parse(readFileSync(report, "utf8")); diff --git a/tests/harness/self-hosting-timing.ts b/tests/harness/self-hosting-timing.ts new file mode 100644 index 000000000..e0b4a0a1f --- /dev/null +++ b/tests/harness/self-hosting-timing.ts @@ -0,0 +1,9 @@ +export async function bootstrapStep(name: string, work: () => Promise): Promise { + console.log(`bootstrap: ${name}`); + const started = performance.now(); + try { + return await work(); + } finally { + console.log(`bootstrap: ${name} took ${((performance.now() - started) / 1000).toFixed(1)}s`); + } +} diff --git a/tests/harness/self-hosting-ts7-client.test.ts b/tests/harness/self-hosting-ts7-client.test.ts index c471bf4ec..bdf174e1a 100644 --- a/tests/harness/self-hosting-ts7-client.test.ts +++ b/tests/harness/self-hosting-ts7-client.test.ts @@ -19,7 +19,7 @@ test("the production TypeScript RPC and filesystem client lowers entirely static expect(coverage.stats.functionsSkipped).toBe(0); }); -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`native TypeScript client talks directly to the pinned parser/checker (${backend})`, async () => { const dir = mkdtempSync(join(tempRoot, "scriptc-native-ts7-")); try { diff --git a/tests/harness/self-hosting-ts7-process.test.ts b/tests/harness/self-hosting-ts7-process.test.ts index ebd744b83..64e39cf06 100644 --- a/tests/harness/self-hosting-ts7-process.test.ts +++ b/tests/harness/self-hosting-ts7-process.test.ts @@ -14,7 +14,7 @@ const oracleEntry = join(root, "tests/fixtures/self-hosting/ts7-client.ts"); const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`the native client owns its TypeScript server from startup through exit (${backend})`, async () => { const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-owned-")); try { diff --git a/tests/harness/self-hosting-type-mapper.test.ts b/tests/harness/self-hosting-type-mapper.test.ts index d7568520c..23810f4da 100644 --- a/tests/harness/self-hosting-type-mapper.test.ts +++ b/tests/harness/self-hosting-type-mapper.test.ts @@ -68,7 +68,7 @@ function checkReport(report: MappingReport): void { ]); } -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`production type mapper runs against native TS7 (${backend})`, async () => { const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-type-mapper-")); try { @@ -129,7 +129,7 @@ for (const backend of ["c", "llvm"] as const) { if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); if (!("binaryPath" in built)) throw new Error("type mapper did not produce a native executable"); expect(built.backend).toBe(backend); - expect(built.llvmRefusal).toBeUndefined(); + const output = join(directory, "native.json"); const native = spawnSync(built.binaryPath, [ts7Executable(), source, output], { encoding: "utf8", timeout: 90_000 }); expect(native.error, native.stderr).toBeUndefined(); diff --git a/tests/harness/self-hosting-union-conversions.test.ts b/tests/harness/self-hosting-union-conversions.test.ts index de5bdbac0..1920074b7 100644 --- a/tests/harness/self-hosting-union-conversions.test.ts +++ b/tests/harness/self-hosting-union-conversions.test.ts @@ -5,8 +5,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { expect, test } from "vitest"; -import { compileC, deserializeModule, emitCModule, validateModule } from "@scriptc/compiler"; -import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js"; +import { compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler"; import type { AnalyzeResult, compile } from "@scriptc/compiler"; const root = join(import.meta.dirname, "../.."); @@ -15,7 +14,7 @@ const execFileAsync = promisify(execFile); const runOptions = { encoding: "utf8" as const, timeout: 60_000, maxBuffer: 32 * 1024 * 1024 }; for (const fixture of ["ir-refinements", "union-conversions"]) { - for (const backend of ["c", "llvm"] as const) { + for (const backend of ["llvm"] as const) { test(`self-hosting ${fixture} executes natively (${backend})`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-union-conversions-")); const exe = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : "")); @@ -47,7 +46,7 @@ for (const fixture of ["ir-refinements", "union-conversions"]) { if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); if (!("binaryPath" in built)) throw new Error("native stage did not produce an executable"); expect(built.backend).toBe(backend); - expect(built.llvmRefusal).toBeUndefined(); + const nodePath = join(dir, "node.json"), nativePath = join(dir, "native.json"); const oracle = spawnSync(process.execPath, ["--import", "tsx", entry, nodePath], { ...runOptions, cwd: root }); const native = spawnSync(built.binaryPath, [nativePath], runOptions); @@ -71,9 +70,9 @@ for (const fixture of ["ir-refinements", "union-conversions"]) { const module = deserializeModule(text); expect(module).toEqual(deserializeModule(readFileSync(nodePath, "utf8"))); expect(validateModule(module)).toEqual([]); - expect(module.functions.filter((fn) => fn.name.endsWith("retag"))).toHaveLength(4); - const cPath = join(dir, backend === "c" ? "generated.c" : "generated.ll"); - writeFileSync(cPath, backend === "c" ? emitCModule(module) : emitLlvmModule(module)); + expect(module.functions.filter((fn) => fn.name.endsWith("retag"))).toHaveLength(5); + const cPath = join(dir, "generated.ll"); + writeFileSync(cPath, emitLlvmModule(module)); await compileC({ cPath, outPath: exe("generated"), sanitize, optimization: "dev" }); const generated = spawnSync(exe("generated"), [], runOptions); expect(generated.error).toBeUndefined(); @@ -85,6 +84,7 @@ for (const fixture of ["ir-refinements", "union-conversions"]) { "n:0 17", "n:1 43", "n:undefined true", "n:invalid TypeError", "n:narrow TypeError", "b:0 17", "b:1 43", "b:undefined true", "b:narrow TypeError", "i:0 17", "i:1 43", "i:0 17", "i:undefined true", "i:invalid TypeError", "i:narrow TypeError", + ...Array.from({ length: 32 }, (_, index) => `large:${index} ${index}`), "extract_bool true", "extract_bool:undefined TypeError", "extract_bool:wrong TypeError", "deferred_bool false", "deferred_bool:present true", "deferred_bool:wrong TypeError", "extract_f64 37", "extract_f64:undefined TypeError", "extract_f64:wrong TypeError", diff --git a/tests/harness/self-hosting-validator-cases.ts b/tests/harness/self-hosting-validator-cases.ts index 5c45f3d8c..1bd9d679c 100644 --- a/tests/harness/self-hosting-validator-cases.ts +++ b/tests/harness/self-hosting-validator-cases.ts @@ -40,6 +40,29 @@ export function validatorCases(): ValidatorCase[] { }, invalid ? "boolLit must be bool" : undefined); } } + for (const arm of ["cond", "then", "else", "mixed"]) { + for (const invalid of [false, true]) { + add(`deep conditional ${arm} ${invalid ? "invalid" : "valid"}`, (m) => { + const nullable: IrType = { kind: "union", unionId: "optional-bool" }; + m.unions = [{ id: "optional-bool", arms: [BOOL, { kind: "undefinedT" }] }]; + let tree: IrExpr = invalid ? { kind: "boolLit", value: true, type: F64, loc } : boolLit(true, loc); + for (let depth = 0; depth < 128; depth++) { + const leaf = boolLit(depth % 2 === 0, loc); + if (arm === "mixed" && depth % 3 === 0) { + tree = { kind: "logical", op: "&&", left: leaf, right: tree, type: BOOL, loc }; + } else if (arm === "mixed" && depth % 3 === 1) { + tree = { kind: "nullish", left: { kind: "unionWrap", unionId: "optional-bool", tag: 1, + value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc }, type: nullable, loc }, + right: tree, type: BOOL, loc }; + } else { + tree = { kind: "ternary", cond: arm === "cond" ? tree : leaf, + then: arm === "then" ? tree : leaf, else_: arm === "else" || arm === "mixed" ? tree : leaf, type: BOOL, loc }; + } + } + m.functions[0]!.body = [expression(tree)]; + }, invalid ? "boolLit must be bool" : undefined); + } + } add("duplicate function", (m) => { m.functions.push(structuredClone(m.functions[0]!)); }, "duplicate function"); add("unknown local", (m) => { m.functions[0]!.body = [expression(varRef("missing", F64, loc))]; }, "missing"); add("parameter without local", (m) => { diff --git a/tests/harness/self-hosting-validator.test.ts b/tests/harness/self-hosting-validator.test.ts index 9bdb8551e..bfceda671 100644 --- a/tests/harness/self-hosting-validator.test.ts +++ b/tests/harness/self-hosting-validator.test.ts @@ -11,7 +11,7 @@ const root = fileURLToPath(new URL("../..", import.meta.url)); const entry = join(root, "tests/fixtures/self-hosting/validate.ts"); const runOptions = { cwd: root, timeout: 30_000, maxBuffer: 16 * 1024 * 1024 }; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`self-hosting full validator: ${backend} matches Node on valid and invalid IR`, async () => { const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-validator-")); try { diff --git a/tests/harness/self-hosting.test.ts b/tests/harness/self-hosting.test.ts index 6f01bd474..8693edd60 100644 --- a/tests/harness/self-hosting.test.ts +++ b/tests/harness/self-hosting.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, test } from "vitest"; -import { analyze, compile, compileC, deserializeModule, emitCModule, validateModule } from "@scriptc/compiler"; +import { analyze, compile, compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler"; const root = fileURLToPath(new URL("../..", import.meta.url)); @@ -22,7 +22,7 @@ test("the complete IR validator lowers statically without skipped functions", () // beyond the fixture directory, which the corpus oracle cache does not hash. // Node executes the actual TS modules through tsx's .js → .ts resolution. for (const component of ["source-locations", "ir-collections", "ir-types", "ir-control-flow", "emitter-literals"]) { - for (const backend of ["c", "llvm"] as const) { + for (const backend of ["llvm"] as const) { test(`self-hosting ${component}: ${backend} matches Node`, async () => { const entry = join(root, "tests/fixtures/self-hosting", `${component}.ts`); const outDir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-self-hosting-")); @@ -61,7 +61,7 @@ for (const component of ["source-locations", "ir-collections", "ir-types", "ir-c // IR must produce a working executable. Comparing its serialized IR // to Node also pins construction order and every recursive payload. for (const [fixture, backend] of ["ir-build", "contextual-ir"].flatMap((fixture) => - (["c", "llvm"] as const).map((backend) => [fixture, backend] as const))) { + (["llvm"] as const).map((backend) => [fixture, backend] as const))) { test(`self-hosting IR generation ${fixture}: ${backend} builds and validates an executable program`, async () => { const entry = join(root, "tests/fixtures/self-hosting", `${fixture}.ts`); const outDir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-ir-build-")); @@ -85,10 +85,10 @@ for (const [fixture, backend] of ["ir-build", "contextual-ir"].flatMap((fixture) expect(native.stderr).toEqual(oracle.stderr); const mod = deserializeModule(native.stdout.toString()); expect(validateModule(mod)).toEqual([]); - const cPath = join(outDir, `generated-${bound}.c`); + const llvmPath = join(outDir, `generated-${bound}.ll`); const outPath = exe(`generated-${bound}`); - writeFileSync(cPath, emitCModule(mod)); - await compileC({ cPath, outPath, sanitize }); + writeFileSync(llvmPath, emitLlvmModule(mod)); + await compileC({ cPath: llvmPath, outPath, sanitize }); const program = spawnSync(outPath, [], options); expect(program.error).toBeUndefined(); expect(program.signal).toBeNull(); diff --git a/tests/harness/server.test.ts b/tests/harness/server.test.ts index 8cedb5739..f7567b70d 100644 --- a/tests/harness/server.test.ts +++ b/tests/harness/server.test.ts @@ -103,7 +103,7 @@ async function build(entry: string): Promise { // Pinned: the listening-process differential was written against the // C lane; keeping lane identity fixed keeps a three-leg diff meaning // "server behavior changed", never "the default backend moved". - backend: "c", + backend: "llvm", }); if (!result.ok) { throw new Error( @@ -169,7 +169,7 @@ describe(`server differential (${cases.length} programs${sanitize ? ", sanitized result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), ); } - const llvm = readFileSync(result.cPath, "utf8"); + const llvm = readFileSync(result.llvmPath, "utf8"); expect(llvm).toContain( "declare void @scr_net_listen_opts_reuse_port(ptr, double, ptr, i1 zeroext, i1 zeroext, ptr)", ); diff --git a/tests/harness/terminal-geometry.test.ts b/tests/harness/terminal-geometry.test.ts index 0aff8d5eb..e29a659ff 100644 --- a/tests/harness/terminal-geometry.test.ts +++ b/tests/harness/terminal-geometry.test.ts @@ -21,7 +21,7 @@ test.skipIf(process.platform === "win32")("stdio geometry matches Node on indepe const nativeEnv = process.platform === "darwin" && process.env["SCRIPTC_SAN"] === "1" ? { ...process.env, MallocNanoZone: "0" } : process.env; - for (const backend of ["c", "llvm"] as const) { + for (const backend of ["llvm"] as const) { const result = await compile(entry, { outDir: dir, outPath: join(dir, backend), backend, sanitize: process.env["SCRIPTC_SAN"] === "1" }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const native = await run("python3", [script, result.binaryPath], { env: nativeEnv }); diff --git a/tests/harness/unicode-host-filesystem.test.ts b/tests/harness/unicode-host-filesystem.test.ts index 084d2066f..bce91a531 100644 --- a/tests/harness/unicode-host-filesystem.test.ts +++ b/tests/harness/unicode-host-filesystem.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { compile } from "@scriptc/compiler"; import { expect, test } from "vitest"; -for (const backend of ["c", "llvm"] as const) { +for (const backend of ["llvm"] as const) { test(`host filesystem preserves Unicode paths and directory entries (${backend})`, async () => { const scratch = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-unicode-host-")); try { diff --git a/tests/harness/vercel-e2e.test.ts b/tests/harness/vercel-e2e.test.ts index 39d3808fc..76ef37379 100644 --- a/tests/harness/vercel-e2e.test.ts +++ b/tests/harness/vercel-e2e.test.ts @@ -151,10 +151,7 @@ async function buildDriver(name: string, entrySource: (rel: string) => string): writeFileSync(join(dir, "tsconfig.json"), DRIVER_TSCONFIG); const binary = join(outDir, "program"); mkdirSync(outDir, { recursive: true }); - // Deliberately NO backend pin: the acceptance build is flagless-user- - // shaped, so it rides the release default — today the npm embedding - // refuses the LLVM tier and the build falls back to the C backend - // transparently; this suite staying green IS the fallback acceptance. + // Exercise the default LLVM path used by ordinary dynamic builds. const result = await compile(entry, { outPath: binary, outDir, sanitize, dynamic: true }); if (!result.ok) { throw new Error( diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 6ee67ce61..01f0f3e7a 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -107,6 +107,7 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { }); test.each([ + "4031-event-emitter-long-tuples.ts", "2700-wasi-core.ts", "1612-cjs-module-globals.cjs", "992-fs-roundtrip.ts", @@ -161,7 +162,7 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { const result = await compile(entry, { outDir, outPath }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); expect(result.backend).toBe("llvm"); - expect(result.cPath.endsWith(".ll")).toBe(true); + expect(result.llvmPath.endsWith(".ll")).toBe(true); expect([...(await readFile(outPath)).subarray(0, 4)]).toEqual([0x00, 0x61, 0x73, 0x6d]); const node = await run(process.execPath, ["--no-warnings", entry]); @@ -206,40 +207,6 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { } }); - test("the explicit C backend still emits compact async-free wasm", async () => { - const outDir = await mkdtemp("/tmp/scriptc-wasi-c-"); - const outPath = join(outDir, "program.wasm"); - const result = await compile(join(repoRoot, "tests/corpus/001-hello.ts"), { - outDir, - outPath, - backend: "c", - }); - if (!result.ok) { - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - } - expect(result.backend).toBe("c"); - const wasm = await readFile(outPath); - expect(wasmCustomSectionNames(wasm).filter((name) => name.startsWith(".debug"))).toEqual([]); - expect(wasm.byteLength).toBeLessThan(256 * 1024); - }); - - test("the explicit C backend diagnoses coroutine-dependent programs", async () => { - const outDir = await mkdtemp("/tmp/scriptc-wasi-c-refusal-"); - const result = await compile(join(repoRoot, "tests/corpus/1020-async-basics.ts"), { - outDir, - outPath: join(outDir, "program.wasm"), - backend: "c", - }); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.diagnostics).toHaveLength(1); - expect(result.diagnostics[0]?.code).toBe("SC3001"); - expect(result.diagnostics[0]?.message).toMatch( - /c backend does not support an async function .* for wasm32-wasi; use --backend llvm/, - ); - } - }); - test("embedded source comments mentioning fetch do not trigger SC3002", async () => { const entry = join(repoRoot, "tests/fixtures/npm/cases/island-web-plumbing/main.ts"); const outDir = await mkdtemp("/tmp/scriptc-wasi-fetch-comment-"); @@ -380,7 +347,7 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { "--import", loader, join(repoRoot, "packages/cli/src/main.ts"), "run", entry, - "--no-keep-c", + "--no-keep-llvm", "-o", join(outDir, "program.wasm"), ]); const node = await execFileAsync(process.execPath, [entry]); diff --git a/tests/harness/web-globals.test.ts b/tests/harness/web-globals.test.ts index 56bc6a7be..47d1fc3f6 100644 --- a/tests/harness/web-globals.test.ts +++ b/tests/harness/web-globals.test.ts @@ -41,10 +41,8 @@ async function compileAndRun(name: string, source: string): Promise { const file = join(outDir, `${name}.ts`); writeFileSync(file, source); // Deliberately NO backend pin: these are flagless-user-shaped --dynamic - // builds, so the suite rides the release default (LLVM where the tier - // claims the program, the transparent C fallback where it refuses). The - // fence MESSAGES asserted below come from the island runtime the two - // backends share, so they are lane-invariant by construction. + // builds through the production LLVM backend. The fence messages + // asserted below come from the island runtime. const result = await compile(file, { outPath: join(outDir, name), outDir, diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index ea6139587..f9e5cc378 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -54,8 +54,9 @@ const corpusDir = join(repoRoot, "tests/corpus"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const target = process.env["SCRIPTC_WIN_TARGET"] ?? "x86_64-windows-gnu"; const host = process.env["SCRIPTC_WIN_HOST"] ?? "windows-dev"; -const laneDirWin = "C:\\Users\\rdp\\work\\scriptc-lane"; // remote commands (cmd.exe) -const laneDirScp = "C:/Users/rdp/work/scriptc-lane"; // scp destinations +const laneNameId = `scriptc-llvm-${process.pid}-${Date.now()}`; +const laneDirWin = `C:\\Users\\rdp\\work\\${laneNameId}`; // remote commands (cmd.exe) +const laneDirScp = `C:/Users/rdp/work/${laneNameId}`; // scp destinations /* ssh connection multiplexing: one master connection per run, every scp/ssh * rides it (~0.1s per op instead of ~0.4s). The control socket lives in the @@ -331,9 +332,7 @@ async function crossCompile(file: string): Promise { const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); const outPath = join(outDir, `${laneName(file)}.exe`); - // Pinned "c" here and at every compile below: the Windows lane is a - // C-reference suite — the cross-compile story is the C backend's. - const result = await compile(file, { outPath, outDir, dynamic: wantsDynamic(file), backend: "c" }); + const result = await compile(file, { outPath, outDir, dynamic: wantsDynamic(file), backend: "llvm" }); if (!result.ok) { throw new Error( "corpus program failed to cross-compile:\n" + @@ -470,7 +469,7 @@ async function shipFixture(c: { name: string; entry: string }): Promise { const key = hash.update("windows\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - const result = await compile(c.entry, { outPath: join(outDir, `${c.name}.exe`), outDir, backend: "c" }); + const result = await compile(c.entry, { outPath: join(outDir, `${c.name}.exe`), outDir, backend: "llvm" }); if (!result.ok) { throw new Error( "fixture failed to cross-compile:\n" + @@ -744,7 +743,7 @@ describe.skipIf(!enabled)(`windows differential (${target})`, () => { const key = hash.update("windows-fetch\0").update(target).digest("hex").slice(0, 16); const outDir = join(cacheDir, key); mkdirSync(outDir, { recursive: true }); - const result = await compile(c.entry, { outPath: join(outDir, `${c.name}.exe`), outDir, dynamic: true, backend: "c" }); + const result = await compile(c.entry, { outPath: join(outDir, `${c.name}.exe`), outDir, dynamic: true, backend: "llvm" }); if (!result.ok) { throw new Error( "fetch fixture failed to cross-compile:\n" + diff --git a/tests/harness/workspace-source.test.ts b/tests/harness/workspace-source.test.ts index 8ab49786d..39a1fc777 100644 --- a/tests/harness/workspace-source.test.ts +++ b/tests/harness/workspace-source.test.ts @@ -11,7 +11,7 @@ const entry = join(repoRoot, "tests/fixtures/workspace-source/main.ts"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/workspace-source"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test.for(["c", "llvm"] as const)( +test.for(["llvm"] as const)( "a TypeScript-source workspace package compiles as program modules with the %s backend", async (backend) => { const outDir = join(cacheDir, `${backend}-${sanitize ? "san" : "plain"}`); diff --git a/tests/library-mode/callbacks/lib_builtins.ts b/tests/library-mode/callbacks/lib_builtins.ts index 3724dc24c..6a11eec02 100644 --- a/tests/library-mode/callbacks/lib_builtins.ts +++ b/tests/library-mode/callbacks/lib_builtins.ts @@ -2,8 +2,7 @@ // declaration-file ambients without claiming those bindings. isNaN must keep // its standard-library lowering; parseInt is unreferenced channel capacity and // must not be validated against lib.d.ts's optional-radix signature. -// `int` is a valid TypeScript identifier but a C keyword: C emission must -// dispatch it indirectly without inventing an `extern ... int(...)` symbol. +// Profile channels dispatch indirectly, including names that are C keywords. declare function int(x: number): number; export function checkBuiltin(x: number): boolean { diff --git a/tests/library-mode/int-returns/profile.json b/tests/library-mode/int-returns/profile.json index dd3787d3f..db7c5efcc 100644 --- a/tests/library-mode/int-returns/profile.json +++ b/tests/library-mode/int-returns/profile.json @@ -2,7 +2,7 @@ "profile_format": 1, "name": "int-returns", "entry": "lib.ts", - "emission": "c", + "emission": "llvm", "abi": { "prefix": "kr_", "init_symbol": "kr_init", diff --git a/tests/library-mode/int-trap/profile.json b/tests/library-mode/int-trap/profile.json index f03fac3d8..7cb1464a9 100644 --- a/tests/library-mode/int-trap/profile.json +++ b/tests/library-mode/int-trap/profile.json @@ -2,7 +2,7 @@ "profile_format": 1, "name": "int-trap", "entry": "lib.ts", - "emission": "c", + "emission": "llvm", "abi": { "prefix": "kt_", "init_symbol": "kt_init", diff --git a/tests/test262/README.md b/tests/test262/README.md index 79a04fae7..7a2bcce79 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -10,7 +10,7 @@ pnpm test:test262 The vendored inputs are regression fixtures for developing the runner and compiler. Variants are checked against `expectations.json`; entries without an expectation require successful completion. A recorded refusal remains a refusal, and a different outcome fails the regression gate. An external snapshot survey treats every executed non-pass as a failing exit status. An all-excluded selection also fails, except for empty distributed shards. -The same regression tests and host assertion checks are included in `pnpm test`. `pnpm test:sandbox` partitions the cases across both plain and sanitized lanes. The default backend may fall back from LLVM to C according to the ordinary compiler policy; every executed result records the actual backend. `--backend llvm` or `--backend c` pins a backend, and `SCRIPTC_SAN=1` enables sanitizers. +The same regression tests and host assertion checks are included in `pnpm test`. `pnpm test:sandbox` partitions the cases across both plain and sanitized lanes. The default backend may fall back from LLVM to C according to the ordinary compiler policy; every executed result records the actual backend. `--backend llvm` or `--backend llvm` pins a backend, and `SCRIPTC_SAN=1` enables sanitizers. ## Full snapshot surveys diff --git a/tests/test262/compile-worker.ts b/tests/test262/compile-worker.ts index d98de3efe..a66fc8334 100644 --- a/tests/test262/compile-worker.ts +++ b/tests/test262/compile-worker.ts @@ -13,7 +13,7 @@ async function main(): Promise { ...(request.backend === "default" ? {} : { backend: request.backend }), }); writeFileSync(request.result, JSON.stringify(result.ok - ? { ok: true, backend: result.backend, llvmRefusal: result.llvmRefusal } + ? { ok: true, backend: result.backend } : { ok: false, diagnostics: result.diagnostics })); } diff --git a/tests/test262/execute.ts b/tests/test262/execute.ts index d5d8c5cf9..2ae94454d 100644 --- a/tests/test262/execute.ts +++ b/tests/test262/execute.ts @@ -12,7 +12,6 @@ export interface Outcome { reason?: string; phase?: string; backend?: string; - llvmRefusal?: string; stdout?: string; stderr?: string; diagnostics?: unknown[]; @@ -62,7 +61,7 @@ export function boundedRun(command: string, args: string[], timeoutMs: number): } export async function runSource(source: string, options: { - backend?: "default" | "llvm" | "c"; + backend?: "default" | "llvm"; asyncTest?: boolean; includes?: string[]; variant?: "strict" | "sloppy"; @@ -103,7 +102,7 @@ export async function runSource(source: string, options: { } phase = "runtime"; const result = await boundedRun(binary, [], options.runtimeTimeoutMs ?? 10_000); - const common = { backend: build.backend, ...(build.llvmRefusal ? { llvmRefusal: build.llvmRefusal } : {}) }; + const common = { backend: build.backend }; if (result.timeout) return finish({ ...common, status: "timeout", phase }); if (result.signal) return finish({ ...common, status: "crash", phase, reason: result.signal, stderr: result.stderr }); const stderr = options.sanitize diff --git a/tests/test262/run.ts b/tests/test262/run.ts index 2e951c583..8b915c792 100644 --- a/tests/test262/run.ts +++ b/tests/test262/run.ts @@ -22,7 +22,7 @@ async function main(): Promise { --filter TEXT select test paths containing TEXT --limit N first N selected test files --workers N concurrent compiles (default 2) - --backend default|llvm|c default uses scriptc's normal selection + --backend default|llvm LLVM is the production backend --report PATH JSON report (default under node_modules/.cache) --journal PATH append each result as JSONL during the run --compile-timeout MS --runtime-timeout MS bound each compiler/program process @@ -39,8 +39,8 @@ Other variants remain visible as exclusions.`); if (!Number.isSafeInteger(n) || n <= 0) throw new Error(`${name} must be a positive integer`); return n; } - if (!["default", "c", "llvm"].includes(values.backend!)) throw new Error("invalid backend"); - const backend = values.backend as "default" | "c" | "llvm"; + if (!["default", "llvm"].includes(values.backend!)) throw new Error("invalid backend"); + const backend = values.backend as "default" | "llvm"; const workers = positive(values.workers ?? process.env.SCRIPTC_TEST_WORKERS, 2, "workers"); const limit = positive(values.limit, Number.MAX_SAFE_INTEGER, "limit"); const compileTimeoutMs = positive(values["compile-timeout"], 120_000, "compile-timeout");