Skip to content

Introduce NightMonkey: an ahead-of-time JS-to-Wasm compiler. - #2

Open
cfallin wants to merge 5 commits into
bytecodealliance:wasi-ff147from
cfallin:nightmonkey
Open

Introduce NightMonkey: an ahead-of-time JS-to-Wasm compiler.#2
cfallin wants to merge 5 commits into
bytecodealliance:wasi-ff147from
cfallin:nightmonkey

Conversation

@cfallin

@cfallin cfallin commented Sep 4, 2026

Copy link
Copy Markdown
Member

NightMonkey is an ahead-of-time JS-to-Wasm compilation tier built inside SpiderMonkey. NIGHT expands to Nonlocal Inference with Guiding Heuristics for Types: an optimistic whole-program type analysis guides code generation, with dynamic guards for correctness. (The night monkey, genus Aotus (really!), is the only truly nocturnal monkey: it does its work in the night, before the program runs during the day.)

Each JS function's bytecode is compiled to a WebAssembly function that runs alongside the runtime compiled to Wasm. There are two modes of use:

  • Snapshot (the shipping flow): the nightmonkey host binary drives Wizer in-process to snapshot the runtime plus loaded user program (or processes an existing Wizer snapshot), reads out JS bytecode and heap objects (such as prototype objects), and rewrites that snapshot with compiled bodies.
  • In-process (the testing flow): the JS shell compiled to Wasm runs under wasm-jit-runner, walks its own live heap, compiles the script tree, and injects the bodies into its running instance via runner hostcalls (--night-inprocess). A drop-in shell for jit-tests.

NightMonkey has a two-part structure: an optimistic static type analysis and a guard-based codegen backend. The idea is that we:

  1. "Predict" types statically, using a model of JavaScript semantics that is intentionally optimistic (elides corner-cases). We call this the "likelier-types analysis" (in a nod to the initial version of the analysis, the "likely-types analysis"; this one is a little better).

  2. Generate an optimistic Wasm body for a given JS function bytecode body, using those predicted types.

  3. Insert dynamic guards checking those assumptions, with fallbacks to a fully generic (but still compiled!) Wasm body.

The key constraint that NightMonkey adheres to, and attempts to solve: we cannot derive type information, or any other profiling information, by observing a running program. In other words, unlike the standard JIT approach based on the "JIT hypothesis" (that a warmed-up program will reach a steady state with stable types, which we can then specialize for), we must decide any specialization we will do ahead-of-time, based on whatever analysis or heuristics we can come up with. The thing we permit ourselves in return is much more analysis time: unlike a JIT engine, we do not need to compile in milliseconds.

NightMonkey performs its analysis using a whole-program, call-sensitive, points-to (heap abstraction) + callgraph analysis, over a lattice that is a hybrid of a Steensgaard (union-find-based) and capped Andersen (points-to-set/membership-based) design.

The codegen using the types that come out of this analysis is then a "two-track" approach: there is one optimistic track that adheres to "type contexts" that are maximally optimal, and one fully generic track. (Earlier experiments tried to do more multiversioning, a la Static Basic Block Versioning, but that did not converge well.)

As of 2026-09-04, comparing to native IonMonkey and baseline tiers, and against Wasm-hosted interpreter and weval+PBL execution:

bench            native-ion nat-baseline  wasm-interp        weval          aot   aot/wasm-int      aot/weval weval/wasm-int      ion/weval        ion/aot   baseline/aot
richards              29205         6489          377          936        11893          31.55          12.71           2.48          31.20           2.46           0.55
deltablue             28179         6870          395          978         6678          16.91           6.83           2.48          28.81           4.22           1.03
crypto                42755         5654          714          949        15696          21.98          16.54           1.33          45.05           2.72           0.36
raytrace              58549        11458         1045         1815        11964          11.45           6.59           1.74          32.26           4.89           0.96
earley-boyer          83262        21153         1510         3982        15088           9.99           3.79           2.64          20.91           5.52           1.40
navier-stokes         43926         8269         1223         2090        24980          20.43          11.95           1.71          21.02           1.76           0.33
splay                 29291        23303         5248         6853        10693           2.04           1.56           1.31           4.27           2.74           2.18
regexp                18601         7223          596          766         2484           4.17           3.24           1.29          24.28           7.49           2.91
pdfjs                 95804        40738         4116         6185        24743           6.01           4.00           1.50          15.49           3.87           1.65
mandreel              73940        11619          865         1269        19545          22.60          15.40           1.47          58.27           3.78           0.59
code-load             70224        69259        37108        37005        37271           1.00           1.01           1.00           1.90           1.88           1.86
box2d                 99321        22370         1896         4135        25999          13.71           6.29           2.18          24.02           3.82           0.86
react-bench           0.631        1.415       15.026       10.421        2.862           5.25           3.64           1.44          16.52           4.54           2.02
geomean               49233        14111         1527         2569        14247           8.93           5.37           1.66          18.94           3.53           1.05
(octane = Score higher-better; react-bench = ms/render lower-better; best-of-3, taskset -c 1)
(ratio cols = speedup of A over B, direction-corrected for react-bench;
 geomean row: lane cols over octane scores only, ratio cols over all benches)

We can conclude that NightMonkey is ~9x faster than the Wasm interpreter on average, or ~5x faster than weval+PBL. It is nearly on par with the native baseline compiler, and within ~3.5x of the IonMonkey optimized native-code ceiling (while running within a Wasm engine). On benchmarks where type-based specialization works especially well, NightMonkey comes within ~2.5x (e.g. Richards) of native Ion.

This work has been done over the past ~6 months with the use of LLM-assisted code generation (mainly Claude Opus and Fable) but with careful line-by-line review and obsessive review of analysis results and generated code. I've read through the whole implementation, checked it to the best of my ability and will continue to do so. It is "a lot" but it does pass jit-tests cleanly. This commit message and the README are fully human-authored.

@cfallin

cfallin commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

(I haven't integrated CI yet as I've been developing locally; I will do this as soon as able) EDIT: CI jobs added in followup commit.

rustc 1.98 adds the OpenEmbedded targets (x86_64-oe-linux-gnu and
friends) to its target list, so an x86_64-pc-linux-gnu host now has two
candidate rust targets and no narrowing step chose between them: the
host vendor "pc" matches neither "unknown" nor "oe", and configure died
with "Don't know how to translate x86_64-pc-linux-gnu for rustc". Prefer
the generic "unknown" vendor when the vendor match is inconclusive.
**NightMonkey** is an ahead-of-time JS-to-Wasm compilation tier built
inside SpiderMonkey. NIGHT expands to *Nonlocal Inference with Guiding
Heuristics for Types*: an optimistic whole-program type analysis guides
code generation, with dynamic guards for correctness. (The night monkey,
genus *Aotus*, is the only truly nocturnal monkey: it does its work in
the night, before the program runs during the day.)

Each JS function's bytecode is compiled to a WebAssembly function that
runs alongside the runtime compiled to Wasm. There are two modes of use:

- **Snapshot** (the shipping flow): the `nightmonkey` host binary drives
  Wizer in-process to snapshot the runtime plus loaded user program (or
  processes an existing Wizer snapshot), reads out JS bytecode and heap
  objects (such as prototype objects), and rewrites that snapshot with
  compiled bodies.
- **In-process** (the testing flow): the JS shell compiled to Wasm runs
  under `wasm-jit-runner`, walks its own live heap, compiles the script
  tree, and injects the bodies into its running instance via runner
  hostcalls (`--night-inprocess`). A drop-in shell for jit-tests.

NightMonkey has a two-part structure: an *optimistic static type
analysis* and a *guard-based codegen backend*. The idea is that we:

1. "Predict" types statically, using a model of JavaScript semantics
   that is intentionally optimistic (elides corner-cases). We call this
   the "likelier-types analysis" (in a nod to the initial version of the
   analysis, the "likely-types analysis"; this one is a little better).

2. Generate an optimistic Wasm body for a given JS function bytecode
   body, using those predicted types.

3. Insert dynamic guards checking those assumptions, with fallbacks to a
   fully generic (but still compiled!) Wasm body.

The *key constraint* that NightMonkey adheres to, and attempts to solve:
we cannot derive type information, or any other profiling information,
by observing a running program. In other words, unlike the standard JIT
approach based on the "JIT hypothesis" (that a warmed-up program will
reach a steady state with stable types, which we can then specialize
for), we must decide any specialization we will do ahead-of-time, based
on whatever analysis or heuristics we can come up with. The thing we
permit ourselves in return is much more analysis time: unlike a JIT
engine, we do not need to compile in milliseconds.

NightMonkey performs its analysis using a whole-program, call-sensitive,
points-to (heap abstraction) + callgraph analysis, over a lattice that
is a hybrid of a Steensgaard (union-find-based) and capped Andersen
(points-to-set/membership-based) design.

The codegen using the types that come out of this analysis is then a
"two-track" approach: there is one optimistic track that adheres to
"type contexts" that are maximally optimal, and one fully generic track.
(Earlier experiments tried to do more multiversioning, a la Static Basic
Block Versioning, but that did not converge well.)

As of 2026-09-04, comparing to native IonMonkey and baseline tiers, and
against Wasm-hosted interpreter and weval+PBL execution:

```plain
bench            native-ion nat-baseline  wasm-interp        weval          aot   aot/wasm-int      aot/weval weval/wasm-int      ion/weval        ion/aot   baseline/aot
richards              29205         6489          377          936        11893          31.55          12.71           2.48          31.20           2.46           0.55
deltablue             28179         6870          395          978         6678          16.91           6.83           2.48          28.81           4.22           1.03
crypto                42755         5654          714          949        15696          21.98          16.54           1.33          45.05           2.72           0.36
raytrace              58549        11458         1045         1815        11964          11.45           6.59           1.74          32.26           4.89           0.96
earley-boyer          83262        21153         1510         3982        15088           9.99           3.79           2.64          20.91           5.52           1.40
navier-stokes         43926         8269         1223         2090        24980          20.43          11.95           1.71          21.02           1.76           0.33
splay                 29291        23303         5248         6853        10693           2.04           1.56           1.31           4.27           2.74           2.18
regexp                18601         7223          596          766         2484           4.17           3.24           1.29          24.28           7.49           2.91
pdfjs                 95804        40738         4116         6185        24743           6.01           4.00           1.50          15.49           3.87           1.65
mandreel              73940        11619          865         1269        19545          22.60          15.40           1.47          58.27           3.78           0.59
code-load             70224        69259        37108        37005        37271           1.00           1.01           1.00           1.90           1.88           1.86
box2d                 99321        22370         1896         4135        25999          13.71           6.29           2.18          24.02           3.82           0.86
react-bench           0.631        1.415       15.026       10.421        2.862           5.25           3.64           1.44          16.52           4.54           2.02
geomean               49233        14111         1527         2569        14247           8.93           5.37           1.66          18.94           3.53           1.05
(octane = Score higher-better; react-bench = ms/render lower-better; best-of-3, taskset -c 1)
(ratio cols = speedup of A over B, direction-corrected for react-bench;
 geomean row: lane cols over octane scores only, ratio cols over all benches)
```

We can conclude that NightMonkey is ~9x faster than the Wasm interpreter on
average, or ~5x faster than weval+PBL. It is nearly on par with the native
baseline compiler, and within ~3.5x of the IonMonkey optimized native-code
ceiling (while running within a Wasm engine). On benchmarks where type-based
specialization works especially well, NightMonkey comes within ~2.5x (e.g.
Richards) of native Ion.

This work has been done over the past ~6 months with the use of
LLM-assisted code generation (mainly Claude Opus and Fable) but with
careful line-by-line review and obsessive review of analysis results and
generated code. I've read through the whole implementation, checked it
to the best of my ability and will continue to do so. It is "a lot" but
it does pass jit-tests cleanly. This commit message and the README are
fully human-authored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant