Nova compiles to WebAssembly (WASM) for universal portability. WASM runs in browsers, on servers (via Wasmtime/Wasmer), and embedded systems. This architecture page compares libraries for generating WASM binaries from Nova's type-checked AST.
Nova can target WASM directly for portability, or optionally use Cranelift for native code generation when maximum performance is needed.
| Library | Purpose | Complexity | Best For |
|---|---|---|---|
| wasm-encoder | WASM binary encoding | Low | Direct WASM generation |
| walrus | WASM transformation | Medium | WASM optimization |
| Cranelift | Native code gen | High | JIT/Native backends |
Part of the Bytecode Alliance ecosystem (same as Wasmtime). Minimal dependencies, direct control over the output binary. Perfect for generating WASM from a custom AST without an intermediate IR.
use wasm_encoder::{
CodeSection, ExportKind, ExportSection, Function, FunctionSection,
Instruction, Module, TypeSection, ValType,
};
fn generate_add_module() -> Vec<u8> {
let mut module = Module::new();
// Type section: define function signature (i32, i32) -> i32
let mut types = TypeSection::new();
types.ty().function(
vec![ValType::I32, ValType::I32], // params
vec![ValType::I32], // results
);
module.section(&types);
// Function section: declare function uses type 0
let mut functions = FunctionSection::new();
functions.function(0); // references type index 0
module.section(&functions);
// Export section: expose "add" function
let mut exports = ExportSection::new();
exports.export("add", ExportKind::Func, 0);
module.section(&exports);
// Code section: function body
let mut codes = CodeSection::new();
let mut f = Function::new(vec![]); // no locals
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::LocalGet(1));
f.instruction(&Instruction::I32Add);
f.instruction(&Instruction::End);
codes.function(&f);
module.section(&codes);
module.finish()
}
pub struct WasmCodegen {
module: Module,
types: TypeSection,
functions: FunctionSection,
exports: ExportSection,
codes: CodeSection,
type_index: u32,
func_index: u32,
}
impl WasmCodegen {
pub fn compile_function(&mut self, func: &nova_ast::Function) {
// Convert Nova types to WASM types
let params: Vec<ValType> = func.params.iter()
.map(|p| self.nova_type_to_wasm(&p.ty))
.collect();
let results: Vec<ValType> = match &func.return_type {
Some(ty) => vec![self.nova_type_to_wasm(ty)],
None => vec![],
};
// Add type signature
self.types.ty().function(params.clone(), results);
let type_idx = self.type_index;
self.type_index += 1;
// Declare function
self.functions.function(type_idx);
let func_idx = self.func_index;
self.func_index += 1;
// Export if public
if func.is_public {
self.exports.export(&func.name, ExportKind::Func, func_idx);
}
// Compile body
let mut wasm_func = Function::new(vec![]);
self.compile_expr(&func.body, &mut wasm_func);
wasm_func.instruction(&Instruction::End);
self.codes.function(&wasm_func);
}
fn compile_expr(&self, expr: &nova_ast::Expr, func: &mut Function) {
match expr {
Expr::Int(n) => {
func.instruction(&Instruction::I32Const(*n as i32));
}
Expr::Binary { op, lhs, rhs } => {
self.compile_expr(lhs, func);
self.compile_expr(rhs, func);
match op {
BinOp::Add => func.instruction(&Instruction::I32Add),
BinOp::Sub => func.instruction(&Instruction::I32Sub),
BinOp::Mul => func.instruction(&Instruction::I32Mul),
BinOp::Div => func.instruction(&Instruction::I32DivS),
};
}
Expr::Var(idx) => {
func.instruction(&Instruction::LocalGet(*idx));
}
// ... more expression types
}
}
fn nova_type_to_wasm(&self, ty: &nova_ast::Type) -> ValType {
match ty {
Type::Int => ValType::I32,
Type::Float => ValType::F64,
Type::Bool => ValType::I32, // booleans as i32
Type::String => ValType::I32, // pointer to memory
_ => panic!("unsupported type"),
}
}
}
If Nova needs to perform WASM-level optimizations or transformations after initial code generation. Powers wasm-bindgen. Preserves DWARF debug info through transformations.
use walrus::{Module, ModuleConfig, ValType, FunctionBuilder, InstrSeqBuilder};
fn build_factorial() -> Module {
let config = ModuleConfig::new();
let mut module = Module::with_config(config);
// Import a logging function from host
let log_ty = module.types.add(&[ValType::I32], &[]);
let (log_fn, _) = module.add_import_func("env", "log", log_ty);
// Define factorial function type: (i32) -> i32
let fact_ty = module.types.add(&[ValType::I32], &[ValType::I32]);
// Build factorial function
let mut builder = FunctionBuilder::new(&mut module.types, &[ValType::I32], &[ValType::I32]);
// Local variables
let n = module.locals.add(ValType::I32);
let result = module.locals.add(ValType::I32);
// Build function body
builder
.func_body()
.i32_const(1)
.local_set(result)
.block(None, |block| {
block.loop_(None, |loop_| {
loop_
.local_get(n)
.i32_eqz()
.br_if(block.id())
.local_get(result)
.local_get(n)
.binop(walrus::ir::BinaryOp::I32Mul)
.local_set(result)
.local_get(n)
.i32_const(1)
.binop(walrus::ir::BinaryOp::I32Sub)
.local_set(n)
.br(loop_.id());
});
})
.local_get(result);
let fact_fn = builder.finish(vec![n], &mut module.funcs);
// Export the function
module.exports.add("factorial", fact_fn);
module
}
For maximum performance when WASM overhead is unacceptable. Cranelift generates native code (x86-64, ARM64, RISC-V) at ~10x faster compilation than LLVM while achieving ~86% of LLVM's runtime performance. 200k LOC vs LLVM's 20M LOC.
use cranelift::prelude::*;
use cranelift_module::{Module, Linkage};
use cranelift_jit::{JITModule, JITBuilder};
fn compile_add_native() -> *const u8 {
// Create JIT module
let builder = JITBuilder::new(cranelift_module::default_libcall_names())?;
let mut module = JITModule::new(builder);
// Define function signature
let mut sig = module.make_signature();
sig.params.push(AbiParam::new(types::I64));
sig.params.push(AbiParam::new(types::I64));
sig.returns.push(AbiParam::new(types::I64));
// Declare function
let func_id = module.declare_function("add", Linkage::Export, &sig)?;
// Create function context
let mut ctx = module.make_context();
ctx.func.signature = sig.clone();
// Build function body with Cranelift IR
let mut builder_ctx = FunctionBuilderContext::new();
let mut builder = FunctionBuilder::new(&mut ctx.func, &mut builder_ctx);
let block = builder.create_block();
builder.append_block_params_for_function_params(block);
builder.switch_to_block(block);
builder.seal_block(block);
// Get parameters and add them
let a = builder.block_params(block)[0];
let b = builder.block_params(block)[1];
let result = builder.ins().iadd(a, b);
builder.ins().return_(&[result]);
builder.finalize();
// Compile and get native function pointer
module.define_function(func_id, &mut ctx)?;
module.finalize_definitions()?;
module.get_finalized_function(func_id)
}
For the initial release, Nova compiles directly to WASM using wasm-encoder.
This provides:
Add walrus-based optimizations for WASM output:
Add optional Cranelift backend for performance-critical applications:
Start with wasm-encoder for direct WASM generation. It's simple, well-maintained by the Bytecode Alliance, and gives full control over the output binary. Add walrus for optimizations when needed, and consider Cranelift for a native backend in v2.
The WASM-first approach aligns with Nova's goals: portability, safety, and AI-friendly execution. WASM sandboxing provides security guarantees that complement Nova's verification system.
WasmCodegen struct with wasm-encoder