Code Generation
The final step: turning verified code into something that runs.
The Final Stage
We've come a long way. Your Nova code has been:
- Lexed — broken into tokens
- Parsed — structured into an AST
- Type checked — verified for type safety
- Contract verified — proven correct
Now comes the final step: code generation — turning that verified AST into something a machine can execute.
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:
Each instruction either:
- Pushes a value onto the stack (
i32.const 3) - Pops values, computes, and pushes result (
i32.add)
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:
fn add(a: Int, b: Int) -> Intbecomes(func $add (param $a i32) (param $b i32) (result i32)a + bbecomeslocal.get $a,local.get $b,i32.add
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:
Type Section
Function signatures: (i32, i32) -> i32
Function Section
Maps function indices to their type signatures
Memory Section
Linear memory for heap data (arrays, strings)
Export Section
Which functions are accessible from outside
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:
- Rich types — strings, records, variants, not just i32/f64
- Interface types — describe APIs between components
- Composition — combine WASM modules like Lego blocks
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:
- Verified properties are preserved — What we prove at compile time holds at runtime
- Capability-based security — WASM's sandbox model matches Nova's capability system
- AI-generated code is contained — Even if AI generates unexpected code, it can't escape the sandbox
- Universal deployment — Browser, server, edge, embedded — one target, all platforms
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 CodeWhat's Next?
You've learned the foundations. Here's where to go from here:
- Read the Architecture docs — Dive deeper into Nova's implementation
- Explore the source — See how everything fits together in Rust
- Contribute — Nova is open source; we'd love your help!
- Build something — The best way to learn is by doing