Architecture

Code Generation

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.

Compilation Pipeline

Nova AST
Nova IR
WASM
Native (optional)

Nova can target WASM directly for portability, or optionally use Cranelift for native code generation when maximum performance is needed.

Library Comparison

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

Option 1: wasm-encoder (Recommended)

Why wasm-encoder?

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.

Complete Example: Addition Function

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()
}

Generating from Nova AST

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"),
        }
    }
}

Option 2: walrus

When to consider walrus

If Nova needs to perform WASM-level optimizations or transformations after initial code generation. Powers wasm-bindgen. Preserves DWARF debug info through transformations.

Building a Module with walrus

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
}

Option 3: Cranelift (Native Backend)

When to consider Cranelift

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.

Key Cranelift Features

Cranelift IR Example

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)
}

Nova's Codegen Strategy

Phase 1: WASM Target (MVP)

For the initial release, Nova compiles directly to WASM using wasm-encoder. This provides:

Phase 2: Optimization (Future)

Add walrus-based optimizations for WASM output:

Phase 3: Native Backend (Future)

Add optional Cranelift backend for performance-critical applications:

Recommendation for Nova

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.

Implementation Plan

  1. Implement WasmCodegen struct with wasm-encoder
  2. Add Nova type → WASM type mapping
  3. Compile basic expressions (arithmetic, locals, function calls)
  4. Add control flow (if/else, loops, blocks)
  5. Implement memory management for strings and arrays
  6. Add WASI imports for I/O operations
  7. Integrate verification results as WASM custom sections

References