From a3b8541f42b0a1cad75726d3eb4bcdf453cf24e5 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Wed, 23 Sep 2026 11:39:32 -0700 Subject: [PATCH] Add NightMonkey support. NightMonkey is an ahead-of-time JS-to-Wasm compiler built on top of SpiderMonkey. This PR adds a `--nightmonkey` CLI flag that selects the NightMonkey variant of the engine, and invokes the NightMonkey compiler (which is a native binary) on a snapshotted image to AOT-compile JS bytecode into Wasm bytecode. Because the NightMonkey compiler itself is a native program, this PR incorporates binaries for four platforms (Linux/x86-64, Linux/aarch64, Windows/x86-64, macOS/aarch64) and bundles them all into the monolithic NPM package, adding ~25MiB. If we instead want to split out per-platform sub-packages and dynamically download them as needed, I am happy to do that instead. --- .github/workflows/main.yml | 14 +++ .github/workflows/nightmonkey-compilers.yml | 80 +++++++++++++++++ .github/workflows/release.yml | 15 ++++ .gitignore | 2 + CMakeLists.txt | 30 +++++++ Makefile | 9 ++ README.md | 28 ++++++ StarlingMonkey | 2 +- embedding/embedding.cpp | 10 +++ package.json | 6 +- src/cli.js | 7 ++ src/componentize.js | 97 ++++++++++++++++++++- test/api.js | 2 + test/bindings.js | 2 + test/builtins.js | 2 + test/builtins/error-async.js | 7 +- test/builtins/error-sync.js | 7 +- test/export-buffers.js | 3 +- test/import-return-buffers.js | 8 +- test/util.js | 1 + test/wasi.js | 3 + types.d.ts | 13 +++ 22 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/nightmonkey-compilers.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94b0812a..39f77c26 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -103,6 +103,7 @@ jobs: - 'release' - 'debug' - 'weval' + - 'nightmonkey' steps: - uses: actions/checkout@v4 with: @@ -154,6 +155,9 @@ jobs: npm run clean npm run build:${{matrix.build-type}} + nightmonkey-compilers: + uses: ./.github/workflows/nightmonkey-compilers.yml + ######## # Test # ######## @@ -162,6 +166,7 @@ jobs: runs-on: ${{ matrix.os }} needs: - build + - nightmonkey-compilers strategy: fail-fast: false matrix: @@ -176,6 +181,7 @@ jobs: - 'release' - 'debug' - 'weval' + - 'nightmonkey' steps: - uses: actions/checkout@v4 @@ -200,6 +206,14 @@ jobs: lib target + - name: Download NightMonkey compilers + if: matrix.build-type == 'nightmonkey' + uses: actions/download-artifact@v4 + with: + pattern: nightmonkey-* + path: lib/nightmonkey + merge-multiple: true + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: ${{matrix.node-version}} diff --git a/.github/workflows/nightmonkey-compilers.yml b/.github/workflows/nightmonkey-compilers.yml new file mode 100644 index 00000000..fac560b9 --- /dev/null +++ b/.github/workflows/nightmonkey-compilers.yml @@ -0,0 +1,80 @@ +name: nightmonkey-compilers + +# Builds the NightMonkey compiler for each supported host from the NightMonkey +# revision and engine version the StarlingMonkey submodule pins, and uploads +# each as an artifact named after its file in lib/nightmonkey/ (see +# NIGHTMONKEY_HOST_ASSETS in src/componentize.js). The compiler is a native +# binary, so unlike the engine it cannot be built once on Linux. +on: + workflow_call: + +defaults: + run: + shell: bash + +jobs: + build: + name: NightMonkey compiler (${{ matrix.asset }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + asset: nightmonkey-x86_64-linux + binary: nightmonkey + - os: ubuntu-24.04-arm + asset: nightmonkey-aarch64-linux + binary: nightmonkey + - os: macos-14 + asset: nightmonkey-aarch64-macos + binary: nightmonkey + - os: windows-2022 + asset: nightmonkey-x86_64-windows.exe + binary: nightmonkey.exe + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Read NightMonkey pins + id: pins + run: | + file=StarlingMonkey/cmake/nightmonkey.cmake + revision="$(awk '/^set\(NIGHTMONKEY_TAG / {gsub(/set\(NIGHTMONKEY_TAG |\)/, ""); print}' "$file")" + engine="$(awk '/^set\(NIGHTMONKEY_ENGINE_VERSION / {gsub(/"/, "", $2); print $2}' "$file")" + echo "revision=$revision" >> "$GITHUB_OUTPUT" + echo "engine=$engine" >> "$GITHUB_OUTPUT" + + - uses: actions/cache@v4 + id: cache + with: + key: ${{ matrix.asset }}-${{ steps.pins.outputs.revision }}-${{ steps.pins.outputs.engine }} + path: ${{ matrix.asset }} + + - uses: actions/checkout@v4 + if: steps.cache.outputs.cache-hit != 'true' + with: + repository: bytecodealliance/nightmonkey + ref: ${{ steps.pins.outputs.revision }} + path: nightmonkey + + - name: Build compiler + if: steps.cache.outputs.cache-hit != 'true' + # Run from the StarlingMonkey tree so that rustup uses its toolchain, as + # the engine build does. + working-directory: StarlingMonkey + run: | + cargo build --release -p nightmonkey --manifest-path ../nightmonkey/Cargo.toml \ + --target-dir ../nightmonkey-target \ + --no-default-features --features "${{ steps.pins.outputs.engine }}" + cp "../nightmonkey-target/release/${{ matrix.binary }}" "../${{ matrix.asset }}" + + - name: Check compiler + run: ./${{ matrix.asset }} --help + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset }} + path: ${{ matrix.asset }} + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7391fb51..1bffb9cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,10 +98,16 @@ jobs: export PRERELEASE_TAG=$(node scripts/semver-get-prerelease.mjs $NEXT_VERSION); echo -e "prerelease-tag=$PRERELEASE_TAG" >> $GITHUB_OUTPUT; + nightmonkey-compilers: + needs: + - meta + uses: ./.github/workflows/nightmonkey-compilers.yml + pack-npm-release: runs-on: ubuntu-24.04 needs: - meta + - nightmonkey-compilers strategy: matrix: rust-version: @@ -138,6 +144,15 @@ jobs: run: | npm install + # The engines are built by `npm pack` below; the NightMonkey compilers + # for every host are built natively by the job above. + - name: Download NightMonkey compilers + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: nightmonkey-* + path: lib/nightmonkey + merge-multiple: true + - name: Create release package working-directory: ${{ needs.meta.outputs.project-dir }} run: | diff --git a/.gitignore b/.gitignore index 66610a23..05375cac 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ examples/hello-world/guest/hello.component.wasm /build-debug /build-release /build-release-weval +/build-release-nightmonkey +/deps .vscode /package-lock.json .idea diff --git a/CMakeLists.txt b/CMakeLists.txt index add51cd5..b1ef8b76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ set(EMBEDDING_DEP "starling-raw.wasm") set(OUTPUT_NAME_RELEASE "starlingmonkey_embedding.wasm") set(OUTPUT_NAME_DEBUG "starlingmonkey_embedding.debug.wasm") set(OUTPUT_NAME_WEVAL "starlingmonkey_embedding_weval.wasm") +set(OUTPUT_NAME_NIGHTMONKEY "starlingmonkey_embedding_nightmonkey.wasm") # Set the appropriate name based on current configuration if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") @@ -34,6 +35,8 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInf elseif(WEVAL) set(OUTPUT_FILENAME ${OUTPUT_NAME_WEVAL}) set(EMBEDDING_DEP "starling-ics.wevalcache") +elseif(NIGHTMONKEY) + set(OUTPUT_FILENAME ${OUTPUT_NAME_NIGHTMONKEY}) else() set(OUTPUT_FILENAME ${OUTPUT_NAME_RELEASE}) endif() @@ -47,6 +50,33 @@ add_custom_target(starlingmonkey_embedding ${OUTPUT_FILENAME} ) +# The NightMonkey compiler is a native binary that must match the engine +# exactly. The package ships one per supported host (built by CI from the +# NightMonkey revision StarlingMonkey pins) in lib/nightmonkey/, named as in +# NIGHTMONKEY_HOST_ASSETS in src/componentize.js; a local build puts the one +# it built there for the host. +if(NIGHTMONKEY) + set(NIGHTMONKEY_HOST_ASSET "") + if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") + set(NIGHTMONKEY_HOST_ASSET "nightmonkey-x86_64-linux") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(NIGHTMONKEY_HOST_ASSET "nightmonkey-aarch64-linux") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64") + set(NIGHTMONKEY_HOST_ASSET "nightmonkey-aarch64-macos") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows" AND CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") + set(NIGHTMONKEY_HOST_ASSET "nightmonkey-x86_64-windows.exe") + endif() + if(NIGHTMONKEY_HOST_ASSET) + add_dependencies(starlingmonkey_embedding nightmonkey_compiler) + add_custom_command(TARGET starlingmonkey_embedding POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${NIGHTMONKEY_BIN} + ${CMAKE_CURRENT_SOURCE_DIR}/lib/nightmonkey/${NIGHTMONKEY_HOST_ASSET} + VERBATIM + ) + endif() +endif() + if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") add_custom_command(TARGET starlingmonkey_embedding POST_BUILD COMMAND ${WASM_TOOLS_BIN} strip ${OUTPUT_FILENAME} -d ".debug_(info|loc|ranges|abbrev|line|str)" -o ${OUTPUT_FILENAME} diff --git a/Makefile b/Makefile index f51852a4..911ab08e 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ all: release debug: lib/starlingmonkey_embedding.debug.wasm lib/spidermonkey-embedding-splicer.js release: lib/starlingmonkey_embedding.wasm lib/spidermonkey-embedding-splicer.js release-weval: lib/starlingmonkey_ics.wevalcache lib/spidermonkey-embedding-splicer.js +release-nightmonkey: lib/starlingmonkey_embedding_nightmonkey.wasm lib/spidermonkey-embedding-splicer.js lib/spidermonkey-embedding-splicer.js: target/wasm32-wasip1/release/splicer_component.wasm crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit | obj lib @$(JCO) new target/wasm32-wasip1/release/splicer_component.wasm -o obj/spidermonkey-embedding-splicer.wasm --wasi-reactor @@ -32,6 +33,12 @@ lib/starlingmonkey_embedding_weval.wasm: $(STARLINGMONKEY_DEPS) | lib cmake -B build-release-weval -DCMAKE_BUILD_TYPE=Release -DUSE_WASM_OPT=OFF -DWEVAL=ON make -j16 -C build-release-weval starlingmonkey_embedding +# Also puts the NightMonkey compiler this build made for the host in +# lib/nightmonkey/ (CI adds those for the other hosts when packaging). +lib/starlingmonkey_embedding_nightmonkey.wasm: $(STARLINGMONKEY_DEPS) | lib + cmake -B build-release-nightmonkey -DCMAKE_BUILD_TYPE=Release -DNIGHTMONKEY=ON + make -j16 -C build-release-nightmonkey starlingmonkey_embedding + lib/starlingmonkey_ics.wevalcache: lib/starlingmonkey_embedding_weval.wasm @cp build-release-weval/starling-raw.wasm/starling-ics.wevalcache $@ @@ -55,6 +62,8 @@ clean: rm lib/spidermonkey-embedding-splicer.js || true rm lib/starlingmonkey_embedding.wasm || true rm lib/starlingmonkey_embedding.debug.wasm || true + rm -r lib/starlingmonkey_embedding_nightmonkey.wasm lib/nightmonkey || true echo "removing cmake outputs" rm build-debug || true rm build-release || true + rm build-release-nightmonkey || true diff --git a/README.md b/README.md index 7e04bdd9..55495972 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,21 @@ AOT compilation can also be configured with the following options: To use a custom (pre-downloaded) [`weval`][weval] binary, set the `wevalBin` option to the path to your desired weval binary. +### NightMonkey AOT Compilation + +[NightMonkey][nightmonkey] is an ahead-of-time JS-to-Wasm compiler: after the Wizer snapshot is taken, it compiles the JS functions in the snapshot to WebAssembly, using a whole-program type analysis with dynamic guards (and falling back to the interpreter where it cannot compile). + +To enable it, set the `enableNightmonkey: true` option or use the `--nightmonkey` CLI flag. It cannot be combined with weval AOT compilation. + +NightMonkey compiles with a native `nightmonkey` binary that must match the engine exactly, so the package includes one for each supported host (Linux x86-64 and AArch64, macOS AArch64 and Windows x86-64), built alongside the engine. A local engine build (`npm run build:nightmonkey`) adds the one it built for the host. It can also be configured with the following options: + +| Option | Type | Example | Description | +|-------------------|------------|---------------|----------------------------------------------------------------| +| `nightmonkeyBin` | `string` | `./nightmonkey` | Path to the NightMonkey compiler matching the engine (CLI: `--nightmonkey-bin`) | +| `nightmonkeyArgs` | `string[]` | `['--stats']` | Extra arguments to pass to the compiler | + +[nightmonkey]: https://github.com/bytecodealliance/nightmonkey + ## Platform APIs The following APIs are available: @@ -241,6 +256,19 @@ export function componentize(opts: { * Use a pre-existing path to the `weval` binary, if present */ wevalBin?: string; + /** + * Enable AoT using NightMonkey (cannot be combined with `enableAot`) + */ + enableNightmonkey?: boolean; + /** + * Path to the NightMonkey compiler; it must match the engine. Defaults to + * the one for the host that ships with the NightMonkey engine. + */ + nightmonkeyBin?: string; + /** + * Extra arguments to pass to the NightMonkey compiler (e.g. `['--stats']`) + */ + nightmonkeyArgs?: string[]; /** * Use a pre-existing path to the `wizer` binary, if present */ diff --git a/StarlingMonkey b/StarlingMonkey index 9dda8ba7..fa4d65f8 160000 --- a/StarlingMonkey +++ b/StarlingMonkey @@ -1 +1 @@ -Subproject commit 9dda8ba7fcda2e17c6795d402f0478cf4c1f7f37 +Subproject commit fa4d65f8bf19a78d5d06de270395f7e11bdebf1a diff --git a/embedding/embedding.cpp b/embedding/embedding.cpp index 503c273d..fc17ef11 100644 --- a/embedding/embedding.cpp +++ b/embedding/embedding.cpp @@ -2,6 +2,9 @@ #include "debugger.h" #include "builtins/web/performance.h" #include "js/Conversions.h" +#ifdef ENABLE_JS_NIGHTMONKEY +#include "runtime/NightRegistration.h" +#endif namespace builtins::web::console { @@ -161,6 +164,13 @@ cabi_realloc(void *ptr, size_t orig_size, size_t org_align, size_t new_size) { __attribute__((export_name("call"))) uint32_t call(uint32_t fn_idx, void *argptr) { if (Runtime.first_call) { +#ifdef ENABLE_JS_NIGHTMONKEY + // Enable the AOT-compiled bodies the NightMonkey snapshot transform + // added, if any; they are not dispatched to before this. + if (!JS::NightActivate(Runtime.cx)) { + Runtime.engine->abort("(call) unable to activate NightMonkey"); + } +#endif content_debugger::maybe_init_debugger(Runtime.engine, true); js::ResetMathRandomSeed(Runtime.cx); Runtime.first_call = false; diff --git a/package.json b/package.json index f1d62895..54aff47c 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,15 @@ "scripts": { "clean": "npm run clean:starlingmonkey", "clean:starlingmonkey": "rm -rf build-release", - "build": "npm run build:release && npm run build:debug && npm run build:weval", + "build": "npm run build:release && npm run build:debug && npm run build:weval && npm run build:nightmonkey", "build:release": "make release", "build:debug": "make debug", "build:weval": "make release-weval", + "build:nightmonkey": "make release-nightmonkey", "test": "vitest run -c test/vitest.ts", "test:release": "vitest run -c test/vitest.ts", "test:weval": "cross-env WEVAL_TEST=1 vitest run -c test/vitest.ts", + "test:nightmonkey": "cross-env NIGHTMONKEY_TEST=1 vitest run -c test/vitest.ts", "test:debug": "cross-env DEBUG_TEST=1 vitest run -c test/vitest.ts", "prepack": "node scripts/prepack.mjs" }, @@ -48,6 +50,8 @@ "lib/starlingmonkey_embedding.debug.wasm", "lib/starlingmonkey_embedding_weval.wasm", "lib/starlingmonkey_ics.wevalcache", + "lib/starlingmonkey_embedding_nightmonkey.wasm", + "lib/nightmonkey", "src", "types.d.ts" ], diff --git a/src/cli.js b/src/cli.js index 62a72a11..a6937f2a 100755 --- a/src/cli.js +++ b/src/cli.js @@ -12,6 +12,8 @@ export async function componentizeCmd(jsSource, opts) { worldName: opts.worldName, runtimeArgs: opts.runtimeArgs, enableAot: opts.aot, + enableNightmonkey: opts.nightmonkey, + nightmonkeyBin: opts.nightmonkeyBin, engine: opts.engine, disableFeatures: opts.disable, preview2Adapter: opts.preview2Adapter, @@ -35,6 +37,7 @@ program .option('-n, --world-name ', 'WIT world to build') .option('--runtime-args ', 'arguments to pass to the runtime') .option('--aot', 'enable AOT compilation') + .option('--nightmonkey', 'enable NightMonkey AOT compilation') .option( '--engine ', 'provide a custom ComponentizeJS engine build path', @@ -62,6 +65,10 @@ program '--weval-bin ', 'specify a path to a local weval binary', ) + .option( + '--nightmonkey-bin ', + 'specify a path to the NightMonkey compiler matching the engine', + ) .option( '--aot-cache-dir ', 'specify a custom AOT weval cache path', diff --git a/src/componentize.js b/src/componentize.js index 66e5942e..815346f4 100644 --- a/src/componentize.js +++ b/src/componentize.js @@ -2,12 +2,12 @@ import { freemem } from 'node:os'; import { TextDecoder } from 'node:util'; import { Buffer } from 'node:buffer'; import { fileURLToPath, URL } from 'node:url'; -import { cwd, stdout, platform } from 'node:process'; +import { cwd, stdout, platform, arch } from 'node:process'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { resolve, join, dirname, relative } from 'node:path'; import { readFile, writeFile, mkdir, rm, stat } from 'node:fs/promises'; -import { rmSync, existsSync } from 'node:fs'; +import { rmSync, existsSync, accessSync, chmodSync, constants } from 'node:fs'; import { createHash } from 'node:crypto'; import oxc from 'oxc-parser'; @@ -49,6 +49,14 @@ const DEFAULT_AOT_CACHE = fileURLToPath( new URL(`../lib/starlingmonkey_ics.wevalcache`, import.meta.url), ); +/** The NightMonkey compiler in lib/nightmonkey/ for each supported host */ +const NIGHTMONKEY_HOST_ASSETS = { + 'linux x64': 'nightmonkey-x86_64-linux', + 'linux arm64': 'nightmonkey-aarch64-linux', + 'darwin arm64': 'nightmonkey-aarch64-macos', + 'win32 x64': 'nightmonkey-x86_64-windows.exe', +}; + /** Default settings for debug options */ const DEFAULT_DEBUG_SETTINGS = { bindings: false, @@ -117,8 +125,17 @@ export async function componentize( debugBuild = debugBuild || debug?.build; enableWizerLogging = enableWizerLogging || debug?.enableWizerLogging; + if (opts.enableAot && opts.enableNightmonkey) { + throw new Error( + 'enableAot (weval) and enableNightmonkey cannot be used together', + ); + } + // Determine the path to the StarlingMonkey binary const engine = getEnginePath(opts); + const nightmonkeyBin = opts.enableNightmonkey + ? getNightmonkeyPath(opts) + : null; // Determine the default features that should be included const features = new Set(); @@ -365,8 +382,45 @@ export async function componentize( throw new Error(err); } + // AOT-compile the JS in the wizened snapshot with NightMonkey. This runs on + // the core module, before the WASI imports are stubbed out and it is wrapped + // into a component. + let snapshotWasmPath = outputWasmPath; + if (opts.enableNightmonkey) { + const compiledWasmPath = join(workDir, 'out.nightmonkey.wasm'); + const nightmonkey = spawnSync( + nightmonkeyBin, + [ + ...(opts.nightmonkeyArgs ?? []), + outputWasmPath, + '-o', + compiledWasmPath, + ], + { + stdio: [null, stdout, 'pipe'], + encoding: 'utf-8', + }, + ); + if (nightmonkey.status !== 0 || nightmonkey.signal || nightmonkey.error) { + let err = `Failed to AOT-compile with NightMonkey (${nightmonkeyBin}):\n${nightmonkey.stderr ?? ''}`; + if (nightmonkey.signal) { + err += `\nProcess was killed by signal: ${nightmonkey.signal}`; + } + if (nightmonkey.error) { + err += `\nProcess error: ${nightmonkey.error.message}`; + } + if (debugBindings) { + err += `\n\nBinary and sources available for debugging at ${workDir}\n`; + } else { + await rm(workDir, { recursive: true }); + } + throw new Error(err); + } + snapshotWasmPath = compiledWasmPath; + } + // Read the generated WASM back into memory - const bin = await readFile(outputWasmPath); + const bin = await readFile(snapshotWasmPath); // Check for initialization errors, by actually executing the binary in // a mini sandbox to get back the initialization state @@ -508,10 +562,47 @@ function getEnginePath(opts) { let engineBinaryRelPath = `../lib/starlingmonkey_embedding${debugSuffix}.wasm`; if (opts.enableAot) { engineBinaryRelPath = '../lib/starlingmonkey_embedding_weval.wasm'; + } else if (opts.enableNightmonkey) { + engineBinaryRelPath = '../lib/starlingmonkey_embedding_nightmonkey.wasm'; } return fileURLToPath(new URL(engineBinaryRelPath, import.meta.url)); } +/** + * Determine the path to the NightMonkey compiler for the host. + * + * The compiler is a native binary that must match the engine exactly, so the + * package ships one per supported host, built alongside the engine. + */ +function getNightmonkeyPath(opts) { + if (opts.nightmonkeyBin) { + return opts.nightmonkeyBin; + } + const asset = NIGHTMONKEY_HOST_ASSETS[`${platform} ${arch}`]; + if (!asset) { + throw new Error( + `No NightMonkey compiler is available for ${platform}/${arch}; set nightmonkeyBin to one matching the engine`, + ); + } + const bin = fileURLToPath( + new URL(`../lib/nightmonkey/${asset}`, import.meta.url), + ); + if (!existsSync(bin)) { + throw new Error( + `NightMonkey compiler not found at ${bin}; build it with \`npm run build:nightmonkey\`, or set nightmonkeyBin to one matching the engine`, + ); + } + // Packaging and artifact transfers do not always preserve the executable bit. + if (platform !== 'win32') { + try { + accessSync(bin, constants.X_OK); + } catch { + chmodSync(bin, 0o755); + } + } + return bin; +} + /** Prepare a work directory for use with componentization */ async function prepWorkDir() { const baseDir = maybeWindowsPath( diff --git a/test/api.js b/test/api.js index 66947f94..34c2dc51 100644 --- a/test/api.js +++ b/test/api.js @@ -11,6 +11,7 @@ import { DEBUG_TRACING_ENABLED, DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, + NIGHTMONKEY_TEST_ENABLED, } from './util.js'; suite('API', () => { @@ -29,6 +30,7 @@ suite('API', () => { worldName: 'test1', debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, }, }, transpile: { diff --git a/test/bindings.js b/test/bindings.js index 80649773..a22b5fa2 100644 --- a/test/bindings.js +++ b/test/bindings.js @@ -10,6 +10,7 @@ import { DEBUG_TRACING_ENABLED, DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, + NIGHTMONKEY_TEST_ENABLED, maybeLogging, } from './util.js'; @@ -81,6 +82,7 @@ suite('Bindings', async () => { disableFeatures: maybeLogging(disableFeatures), debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, }); const map = { diff --git a/test/builtins.js b/test/builtins.js index b87fc917..5be51d73 100644 --- a/test/builtins.js +++ b/test/builtins.js @@ -11,6 +11,7 @@ import { DEBUG_TRACING_ENABLED, DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, + NIGHTMONKEY_TEST_ENABLED, maybeLogging, } from './util.js'; @@ -42,6 +43,7 @@ suite('Builtins', async () => { sourceName: `${name}.js`, debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, enableFeatures, disableFeatures: maybeLogging(disableFeatures), }, diff --git a/test/builtins/error-async.js b/test/builtins/error-async.js index 70074bb5..22e3bb27 100644 --- a/test/builtins/error-async.js +++ b/test/builtins/error-async.js @@ -1,5 +1,7 @@ import { strictEqual } from 'node:assert'; +import { NIGHTMONKEY_TEST_ENABLED } from '../util.js'; + export const source = ` export async function run () { await new Promise(resolve => setTimeout(resolve, 1)); @@ -14,6 +16,9 @@ export async function test(run) { const err = e.stderr.split('\n'); strictEqual(err[0], 'panic'); strictEqual(err[1], 'Stack:'); - strictEqual(err[2], ' run@error-async.js:4:11'); + // Frames of NightMonkey-compiled functions are not in the stack. + if (!NIGHTMONKEY_TEST_ENABLED) { + strictEqual(err[2], ' run@error-async.js:4:11'); + } } } diff --git a/test/builtins/error-sync.js b/test/builtins/error-sync.js index 51b4b005..b1946de7 100644 --- a/test/builtins/error-sync.js +++ b/test/builtins/error-sync.js @@ -1,5 +1,7 @@ import { strictEqual } from 'node:assert'; +import { NIGHTMONKEY_TEST_ENABLED } from '../util.js'; + export const source = ` export function run () { throw new Error('panic'); @@ -13,6 +15,9 @@ export async function test(run) { const err = e.stderr.split('\n'); strictEqual(err[0], 'panic'); strictEqual(err[1], 'Stack:'); - strictEqual(err[2], ' run@error-sync.js:3:11'); + // Frames of NightMonkey-compiled functions are not in the stack. + if (!NIGHTMONKEY_TEST_ENABLED) { + strictEqual(err[2], ' run@error-sync.js:3:11'); + } } } diff --git a/test/export-buffers.js b/test/export-buffers.js index 2c54ca21..8dd6459f 100644 --- a/test/export-buffers.js +++ b/test/export-buffers.js @@ -2,7 +2,7 @@ import { componentize } from '@bytecodealliance/componentize-js'; import { transpile } from '@bytecodealliance/jco'; import { assert, beforeAll, suite, test } from 'vitest'; -import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, maybeLogging } from './util.js'; +import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, NIGHTMONKEY_TEST_ENABLED, maybeLogging } from './util.js'; const source = ` let saved; @@ -33,6 +33,7 @@ beforeAll(async () => { ]), debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, }); const { files } = await transpile(component, { name: 'export-buffers', diff --git a/test/import-return-buffers.js b/test/import-return-buffers.js index 1a4f17c4..4bca301e 100644 --- a/test/import-return-buffers.js +++ b/test/import-return-buffers.js @@ -4,7 +4,11 @@ import { assert, test } from 'vitest'; import { splicer } from '../lib/spidermonkey-embedding-splicer.js'; -import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED } from './util.js'; +import { + DEBUG_TEST_ENABLED, + NIGHTMONKEY_TEST_ENABLED, + WEVAL_TEST_ENABLED, +} from './util.js'; test('frees an imported string and its return area after copying', async () => { const wit = ` @@ -18,6 +22,8 @@ world test { `; const engineName = WEVAL_TEST_ENABLED ? 'starlingmonkey_embedding_weval.wasm' + : NIGHTMONKEY_TEST_ENABLED + ? 'starlingmonkey_embedding_nightmonkey.wasm' : `starlingmonkey_embedding${DEBUG_TEST_ENABLED ? '.debug' : ''}.wasm`; const engine = await readFile(new URL(`../lib/${engineName}`, import.meta.url)); const { jsBindings } = splicer.spliceBindings( diff --git a/test/util.js b/test/util.js index ba302caa..e3bbf1ea 100644 --- a/test/util.js +++ b/test/util.js @@ -10,6 +10,7 @@ export const DEBUG_TRACING_ENABLED = isEnabledEnvVar(env.DEBUG_TRACING); export const LOG_DEBUGGING_ENABLED = isEnabledEnvVar(env.LOG_DEBUGGING); export const DEBUG_TEST_ENABLED = isEnabledEnvVar(env.DEBUG_TEST); export const WEVAL_TEST_ENABLED = isEnabledEnvVar(env.WEVAL_TEST); +export const NIGHTMONKEY_TEST_ENABLED = isEnabledEnvVar(env.NIGHTMONKEY_TEST); function isEnabledEnvVar(v) { return ( diff --git a/test/wasi.js b/test/wasi.js index be9883a8..ead19c2c 100644 --- a/test/wasi.js +++ b/test/wasi.js @@ -15,6 +15,7 @@ import { DEBUG_TRACING_ENABLED, DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED, +NIGHTMONKEY_TEST_ENABLED, } from './util.js'; suite('WASI', () => { @@ -40,6 +41,7 @@ suite('WASI', () => { worldName: 'test1', debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, }, }, transpile: { @@ -69,6 +71,7 @@ suite('WASI', () => { worldName: 'test1', debugBuild: DEBUG_TEST_ENABLED, enableAot: WEVAL_TEST_ENABLED, + enableNightmonkey: NIGHTMONKEY_TEST_ENABLED, }, }, transpile: { diff --git a/types.d.ts b/types.d.ts index 8ed60047..9ddfb47f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -29,6 +29,19 @@ interface ComponentizeOptions { * Use a pre-existing path to the `weval` binary, if present */ wevalBin?: string; + /** + * Enable AoT using NightMonkey (cannot be combined with `enableAot`) + */ + enableNightmonkey?: boolean; + /** + * Path to the NightMonkey compiler; it must match the engine. Defaults to + * the one for the host that ships with the NightMonkey engine. + */ + nightmonkeyBin?: string; + /** + * Extra arguments to pass to the NightMonkey compiler (e.g. `['--stats']`) + */ + nightmonkeyArgs?: string[]; /** * Use a pre-existing path to the `wizer` binary, if present */