Lesson 7

Code Generation

The final step: turning verified code into something that runs.

30 min read Intermediate

The Final Stage

We've come a long way. Your Nova code has been:

Now comes the final step: code generation — turning that verified AST into something a machine can execute.

Nova Code
Verified AST
Codegen
WebAssembly
Runs Everywhere

Why WebAssembly?

Nova compiles to WebAssembly (WASM), a portable binary format designed to run at near-native speed. Why WASM instead of x86 or ARM machine code?

🌐
Web Browsers

Chrome, Firefox, Safari, Edge all run WASM natively

🖥️
Server / Desktop

Run with Node.js, Deno, Wasmtime, Wasmer

📱
Mobile

iOS and Android via embedded runtimes

☁️
Edge / Serverless

Cloudflare Workers, Fastly Compute

Write once, run anywhere. Your Nova code compiles to a single .wasm file that works on any platform with a WASM runtime.

Think of WASM Like a Universal Translator

Instead of compiling to x86 (Intel/AMD), ARM (Apple Silicon/phones), and RISC-V separately, you compile once to WASM. Each platform's WASM runtime translates it to native code. It's like writing a book once and having it automatically translated to every language.

How WASM Works: Stack Machines

WebAssembly is a stack-based virtual machine. Instead of registers, it uses a stack to pass values between operations.

Let's see how 3 + 5 executes:

Empty stack
i32.const 3
3
Push 3
i32.const 5
5
3
Push 5
i32.add
8
Pop both, push sum

Each instruction either:

From Nova to WASM

Let's trace how a simple function gets compiled:

// Nova source
fn add(a: Int, b: Int) -> Int {
    a + b
}
;; WebAssembly output (text format)
(func $add (param $a i32) (param $b i32) (result i32)
    local.get $a    ;; Push a onto stack
    local.get $b    ;; Push b onto stack
    i32.add         ;; Pop both, push sum
)

The structure maps almost directly:

A More Complex Example

// Nova source
fn factorial(n: Int) -> Int {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}
;; WebAssembly output
(func $factorial (param $n i32) (result i32)
    ;; if n <= 1
    local.get $n
    i32.const 1
    i32.le_s              ;; signed less-than-or-equal
    if (result i32)
        ;; then: return 1
        i32.const 1
    else
        ;; else: return n * factorial(n - 1)
        local.get $n
        local.get $n
        i32.const 1
        i32.sub
        call $factorial       ;; recursive call
        i32.mul
    end
)

The Code Generator in Rust

Nova uses the wasm-encoder crate to emit WASM bytecode. Here's a simplified view of how it works:

use wasm_encoder::{Module, CodeSection, Function, Instruction};

fn compile_expr(expr: &Expr, func: &mut Function) {
    match expr {
        // Literals: push constant onto stack
        Expr::Literal(Literal::Int(n)) => {
            func.instruction(&Instruction::I32Const(*n as i32));
        }

        // Variables: load from local
        Expr::Var(name) => {
            let local_index = lookup_local(name);
            func.instruction(&Instruction::LocalGet(local_index));
        }

        // Binary expressions: compile both sides, then operator
        Expr::Binary { left, op, right } => {
            compile_expr(left, func);   // Left value on stack
            compile_expr(right, func);  // Right value on stack

            match op {
                BinaryOp::Add => func.instruction(&Instruction::I32Add),
                BinaryOp::Sub => func.instruction(&Instruction::I32Sub),
                BinaryOp::Mul => func.instruction(&Instruction::I32Mul),
                BinaryOp::Div => func.instruction(&Instruction::I32DivS),
            }
        }

        // If expressions: structured control flow
        Expr::If { condition, then_branch, else_branch } => {
            compile_expr(condition, func);

            func.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
            compile_expr(then_branch, func);

            if let Some(else_expr) = else_branch {
                func.instruction(&Instruction::Else);
                compile_expr(else_expr, func);
            }

            func.instruction(&Instruction::End);
        }

        // Function calls: compile args, then call
        Expr::Call { name, args } => {
            for arg in args {
                compile_expr(arg, func);  // Args on stack
            }
            let func_index = lookup_function(name);
            func.instruction(&Instruction::Call(func_index));
        }
    }
}

The key insight: the AST structure guides code generation. We recursively walk the tree, emitting instructions as we go. The stack-based nature of WASM means we just need to emit instructions in the right order — the stack handles the plumbing.

WASM Module Structure

A WASM module isn't just functions. It has several sections:

1

Type Section

Function signatures: (i32, i32) -> i32

2

Function Section

Maps function indices to their type signatures

3

Memory Section

Linear memory for heap data (arrays, strings)

4

Export Section

Which functions are accessible from outside

5

Code Section

The actual function bytecode

fn build_module(program: &Program) -> Vec<u8> {
    let mut module = Module::new();

    // Type section: declare function signatures
    let mut types = TypeSection::new();
    for func in &program.functions {
        types.function(func.params, func.results);
    }
    module.section(&types);

    // Function section: map functions to types
    let mut functions = FunctionSection::new();
    for (i, _) in program.functions.iter().enumerate() {
        functions.function(i as u32);
    }
    module.section(&functions);

    // Export section: make functions visible
    let mut exports = ExportSection::new();
    exports.export("main", ExportKind::Func, 0);
    module.section(&exports);

    // Code section: function bodies
    let mut code = CodeSection::new();
    for func in &program.functions {
        let mut f = Function::new([]);
        compile_expr(&func.body, &mut f);
        f.instruction(&Instruction::End);
        code.function(&f);
    }
    module.section(&code);

    module.finish()
}

Running Your WASM

Once you have a .wasm file, you can run it anywhere:

In the Browser

// JavaScript
const response = await fetch('program.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);

// Call the exported function
const result = instance.exports.add(3, 5);
console.log(result);  // 8

In Node.js

const fs = require('fs');
const bytes = fs.readFileSync('program.wasm');
const { instance } = await WebAssembly.instantiate(bytes);

console.log(instance.exports.add(3, 5));  // 8

With Wasmtime (Native)

# Command line
$ wasmtime program.wasm --invoke add 3 5
8

Performance

WASM isn't interpreted — it's JIT-compiled to native code by the runtime. Your Nova code runs at near-native speed, typically within 1-2x of hand-optimized C. And because it's sandboxed, it's secure by default.

Future: WASM Component Model

WebAssembly is evolving. The Component Model adds:

Nova is designed with the Component Model in mind. As WASM evolves, Nova's output will too — giving you access to cutting-edge capabilities while maintaining backward compatibility.

Why WASM for Nova?

We chose WebAssembly as Nova's target for specific reasons that align with our verification goals.

The Problem

Native compilation targets (x86, ARM) have undefined behavior, memory unsafety, and platform-specific quirks. Verified code can still crash due to runtime environment issues.

How Nova Solves It

WASM provides a sandboxed, deterministic execution model. Memory is bounds-checked, there's no undefined behavior, and the same code runs identically everywhere. Verified code stays verified.

This matters for Nova because:

As we build out Nova's capability system, WASM's permission model will let us enforce that verified code only accesses what it's allowed to — from files to network to system calls.

Key Takeaway

Code generation transforms your verified AST into WebAssembly bytecode. WASM's stack-based model makes code generation straightforward — just emit instructions in the right order. The result is portable, fast, and secure code that runs anywhere.

Congratulations!

You've completed the Learn Nova course. You now understand the complete journey from source code to running program: lexing, parsing, type checking, verification, and code generation.

Explore the Source Code

What's Next?

You've learned the foundations. Here's where to go from here: