A RV64GC interpreter and JavaScript JIT compiler written in Rust, compiled to wasm32-unknown-unknown and exposed to JavaScript/Node.js via wasm-bindgen.
The primary execution model is a tracing JIT: for each program counter address, vane generates a JavaScript function string (via j(pc)), compiles it with new Function(...), caches the result, and falls back to the interpreter (interp) on code generation failure. There is no native binary output; the "JIT" target is JavaScript source code that runs inside the same WASM host.
- Interprets RV64IMAC instructions (the
interpasync method onReactor) inside WebAssembly - Compiles RV64IMAC instruction sequences to JavaScript source code strings at runtime (the
j/jit_codemethod) - Executes those generated JS strings via
new Function(...)(thejit_runmethod) - Manages a 64KB-page-based virtual memory system (
Mem) with on-demand page allocation - Exports the
Reactortype viawasm-bindgenso JavaScript can create and drive emulator instances
The WASM JIT target (vane-target-wasm) exists as a stub with only a trait definition; it does not generate real WASM output yet.
| Crate | Purpose |
|---|---|
vane |
Root cdylib. Wires together vane-riscv and vane-meta-gen to produce the Reactor WASM-bindgen type. Contains the interpreter loop (interp). |
vane-arch |
Architecture-agnostic infrastructure: Mem (paged memory), JitCtx trait, TemplateJit (JS codegen driver), CoreJS (JS wrapper/prologue generator), PagingMode, and the renders! / cfg_target_js! / cfg_target_wasm! macros. |
vane-riscv |
RV64 back-end. Implements the RiscvDisplay trait on TemplateJit to emit JavaScript for each RISC-V instruction. Also contains the hint module for detecting RISC-V HINT instructions. |
vane-aarch64 |
Explicitly selected AArch64 frontend using disarm64 0.1.26. Aarch64Reactor executes StackOp traces against a caller-provided state host without changing the RV64 Reactor ABI. |
vane-target-js |
JS codegen helpers. Defines the Flate trait used to optionally minify identifier names in generated JS (ReleaseFlate substitutes short names like f, g, s, u, d; DebugFlate is a passthrough). |
vane-target-wasm |
Stub crate. Declares WasmJit, WasmJitCtx, and JitOpcode traits/types. The actual WASM JIT implementation is not yet written. |
vane-meta-gen |
Procedural-macro-like glue. Exports the vane_meta! declarative macro that generates the Reactor and Core structs along with all wasm-bindgen method impls (register access, page table control, jit_code, jit_run, paging mode setters, etc.). |
Mem uses a BTreeMap<u64, Box<[u8; 65536]>> (64KB pages keyed by page number) with three selectable paging modes:
- Legacy (default): pages are allocated on demand when
get_pageis called - Shared: an explicit two-level page table stored at a known virtual address, compatible with the
rift/r52x/speetfamily - Both: the shared page table is stored inside the legacy virtual address space (nested paging)
Both single-level and three-level (L3/L2/L1) variants exist, in 32-bit and 64-bit entry sizes. The JS codegen in CoreJS emits an inline data(addr) function that performs the appropriate translation for the selected paging mode.
For each pc, jit_code(pc) returns a JS string of the form:
return async function() {
let f=$.f, g=0xffff_ffffn, s=(a=>BigInt.asIntN(64,a)), u=(a=>BigInt.asUintN(64,a)),
data=(p=>{p=$.get_page(p);return new DataView($._sys(`memory`).buffer,p);});
<label>: for(;;) {
const p=<pc>n;
if(data(p).getUint32(0,true)!==<expected_encoding>){delete $.p[`<root>`];return J(p);}
<instruction JS>;
<next label or break>;
}
}
The generated code:
- Reads registers from a plain JS object (
$.r) via string keys (x1..x31;x0always reads0n) - Uses JavaScript
BigIntfor all 64-bit arithmetic - Validates the instruction encoding at the top of each block and invalidates the cache entry if the code has changed (self-modifying code support)
- Falls back to
interpviaJ(pc)on any decode error or unsupported instruction
The interpreter covers the full RV64IMAC base plus M extension: all integer arithmetic, shifts, branches, loads/stores (byte through doubleword), JAL/JALR, LUI/AUIPC, FENCE (as a no-op), ECALL, and all W-suffix 32-bit word variants. Unrecognised instructions return a JS error.
The JS codegen covers the same instruction set. MULH, MULHU, MULHSU use JS BigInt 128-bit shift. Divide-by-zero returns the RISC-V-specified sentinel value.
Floating-point (F/D extensions) and atomics (A extension) are not implemented.
The shared StackOp runtime now supports scalar raw-bit F32/F64 operations.
RV64 supports FLW/FLD, FSW/FSD, FADD.{S,D}, FSUB.{S,D},
FMUL.{S,D}, FDIV.{S,D}, and FSQRT.{S,D} in its trace frontend.
AArch64 supports the matching initial FLOATDP1/FLOATDP2 scalar subset
(FADD, FSUB, FMUL, FDIV, FNMUL, FMIN/FMINNM, FMAX/FMAXNM,
FABS, FNEG, FSQRT, and register FMOV) for S and D values. F32
operations round through the F32 representation after each operation.
AArch64 remains an opt-in library frontend, not a replacement for the current
WASM-bindgen RV64 reactor. It covers extended-register add/subtract, logical
immediates and flags, divide, variable shifts/rotate, MADD/MSUB, long
multiply-add/subtract, SMULH/UMULH, and BRK in addition to the earlier integer
subset. Its scalar FP support also includes the documented non-fused FMADD
family policy, immediate moves, compares/selects, and the basic
FMOV/SCVTF/UCVTF/FCVTZS/FCVTZU integer boundary forms. Native
execution is covered by raw-word tests, a headless-Chrome wasm-pack CoreJS
micro-corpus smoke test, and a Node Function parser check. A broader browser
corpus is still pending. It still traps rounding-mode FP variants and scalar
FP memory; Vane's WASM JIT remains out of scope.
Tests require wasm-bindgen-test and run in-browser only (configured via wasm_bindgen_test_configure!(run_in_browser)). The test suite (crates/vane/_tests/rv_corpus.rs) loads ELF binaries from the rv-corpus git submodule, maps PT_LOAD segments into Mem, and runs them through either the interpreter or JIT. A test passes when execution terminates via ecall with a7=93 (the Linux exit syscall number), which vane treats as a success sentinel.
Current test cases:
- RV64I basic 64-bit operations (via both JIT and interpreter)
- RV64IM multiply/divide (via JIT)
- RV32I NOP and HINT instructions (with and without
test_modeHINT logging)
To run tests:
# AArch64 native StackOp coverage
RUSTC_BOOTSTRAP=1 cargo test -p vane-aarch64 --lib --offline
# AArch64 generated CoreJS execution in headless Chrome
wasm-pack test --headless --chrome crates/vane-aarch64
# Build the RV64 WASM package first
wasm-pack build crates/vane
# Run in-browser tests
wasm-pack test --headless --firefox crates/vane
The submodule at crates/vane/_tests/rv-corpus must be checked out (git submodule update --init).
- Rust toolchain with
wasm32-unknown-unknowntarget wasm-pack(for building the cdylib and running tests)wasm-bindgen-test-runner(configured in.cargo/config.tomlas the wasm test runner)
The workspace sets opt-level = "s" in the release profile.
Vane can compile JIT output for a consistently property-mangled $ runtime
object. Canonical code generation remains the default; use the documented
name-aware rendering APIs only when the host uses a complete alternate scheme.
See JIT_HOST_METHOD_NAMES.md.
- Interpreter: implemented and tested for RV64IM
- JS JIT: implemented and tested for RV64IM
- WASM JIT: stub only, not functional
- Floating-point: scalar raw-bit F32/F64 runtime plus documented RV64/AArch64 trace subsets; rounding-mode variants and scalar AArch64 FP memory remain unsupported
- Atomics: not implemented
- System/privilege modes: not implemented;
ecallhandling is minimal (only syscall 93 is meaningful)
License: MPL-2.0