The Burn VM
The Burn Virtual Machine (bvm)
bvm is the virtual machine that burn run and burni use. It isn't tied to Burn: it has its own instruction set,
a text assembly language, a binary bytecode format, a verifier, a JIT and an API, so other languages can use it as
their backend. Burn is one front end among many that could target it. bvm is written in Burn itself
(compiler/src/bvm/ in the Burn repository), and so is its runtime library
(lib/rt/).
your language ─┐
Burn ──────────┼─▶ bvm module ──▶ verifier ──▶ linker + fusion ──▶ interpreter ──▶ JIT (hot code)
hand-written │ (.bvm text or │ │
assembly ──────┘ .bvmc bytecode) runtime library ◀──┴────────────────┘ (memory, strings, arrays, maps, I/O, threads)What a language gets by targeting bvm:
- A reference-counted heap with strings, growable arrays, hash maps, records and boxed values, plus a library of about 100 runtime functions for text, collections, math, time, files, JSON, HTTP and threads.
- A verifier that rejects malformed code before it runs: stack underflow, stack height mismatches between paths, jumps outside the function, bad local, global, function, type and table references, and code that can run off the end of a function.
- Fast execution. Verified code runs on an interpreter with a precomputed frame size per function, so pushes and pops aren't bounds-checked. At load time common instruction sequences are fused into superinstructions, and functions and loops that run often are compiled to x86-64 or arm64 machine code while the program runs (see the JIT).
- Two interchangeable file formats: readable assembly (
.bvm) and compact bytecode (.bvmc). They convert both ways without loss. - Separate compilation and linking. Modules can declare
externfunctions, carry typed signatures and annotations, and be linked by name (see linking). - Archives. A
.barbundles an application's modules and resources into one file that runs wherever bvm runs: write once, run everywhere (see archives). - Mixins.
@Inject,@Overwriteand@Redirectchange existing functions' bytecode when modules are linked (see mixins). - Native interop. Libraries export functions to programs, call back into them, and use host functions from the program that embeds bvm (see native interop).
- Host functions. Programs can import functions that the embedding Burn program provides.
- Threads.
spawnruns a function on its own thread and returns a future that can be awaited.
Hello, bvm
func main()
str "Hello from bvm!"
rt print
pop
retv
endbvm hello.bvm # assemble, verify and run
bvm asm hello.bvm -o hello.bvmc # write bytecode
bvm hello.bvmc # run the bytecode
bvm dis hello.bvmc # turn bytecode back into text
bvm check hello.bvm # verify only
bvm link app.bvm lib.bvmc -o app.bvmc
bvm pack app.bvmc lib.bvmc -o app.bar && ./app.bar
bvm runtime # list the runtime functions available to `rt`burn and burni run .bvm and .bvmc files too.
Burn on bvm
Every Burn program becomes a bvm module:
burnc app.bn --target bvm -o app.bvmc --emit-asm app.bvm
bvm app.bvmc
burn dump --bvm app.bn # print the module as assemblyA Burn struct becomes a struct type, an interface method becomes a dispatch table, async calls become
spawn, and every place that can fail at runtime carries a source location. Running a .bvmc file skips
parsing and type checking.
Values
Every value is one 64-bit word. What the word means is decided by the instructions that use it:
| Kind | Representation |
|---|---|
| int | a two's complement i64 |
| float | the bits of an f64 (const 1.5 stores those bits) |
| bool | 0 or 1 |
| null | 0 |
| function | its index, as produced by fref and used by calli |
| string, array, map, record, box, future | a pointer to a heap object |
There are separate integer, unsigned and float instructions, as in WebAssembly, so the VM never guesses a
type. Heap objects carry a type id, which is what dispatch, to_str, is_type and error messages use.
Values and the runtime library covers the object layouts and the runtime functions.
What the verifier guarantees
A module that passes verification can't corrupt the VM's own state: the operand stack stays within its frame,
every jump lands on an instruction, and every index names something that exists. It doesn't check value types,
because words are untyped. The VM checks what it can cheaply at runtime (getf on null or on something that
isn't a record, index on something that isn't an array, a division by zero, an out-of-bounds index, a
calli with the wrong number of arguments) and reports a runtime error. Beyond that, a front end must not
use an integer as a pointer. Native machine code has the same rule.