burn / docs

How Burn works

The toolchain

One executable, several names

An installed toolchain looks like this:

~/.burn/
├── bin/
│   ├── burn             the toolchain: one native executable, written in Burn
│   ├── burni -> burn
│   ├── burnc -> burn
│   ├── burn-lsp -> burn
│   ├── bvm -> burn
│   └── burnfmt          a script: burni share/burn/tools/fmt.bn "$@"
└── share/burn/
    ├── tools/fmt.bn     the formatter
    └── lib/             the runtime and bvm for native executables

One executable under several names

bin/burn is compiler/src/bin/burn.bn compiled to native code. It contains the command line (compiler/src/cli/), the whole compiler, the language server, burn doc, and bvm with its JIT (compiler/src/bvm/), linked with the runtime written in Burn (lib/rt/). When it starts, it looks at the name it was started with: as burn, burni, burnc or burn-lsp it is the command line in that role, and under any other name, such as the bvm link, it is the virtual machine with its own commands (bvm app.bvmc, bvm asm, bvm link, ...).

How the command line runs programs

To run a program, the command line checks it and compiles it to a bvm module, then hands the module to the bvm compiled into the same executable (compiler/src/cli/host.bn), which links, verifies and runs it. So burn app.bn never starts a second process and never writes a file. The same layer writes .bvmc bytecode and .bar archives, reads the exported functions of bytecode libraries a program imports, and keeps a REPL session's module and globals between inputs.

burn build produces assembly in Burn and then does start one process: the C compiler, to assemble and link (native executables).

The standard library and the runtime

  • The standard library (lib/std/*.bn) is embedded into the compiler as source text (compiler/src/project/stdlib.bn, generated by compiler/src/bin/genstd.bn), so import "std/strings" never reads a file and always matches the compiler. burn sources writes these sources to disk for editors.
  • The runtime (lib/rt/*.bn) is written in Burn too: memory and the cycle collector, strings, collections, printing, files, processes, JSON, HTTP and tasks. It is compiled into the toolchain, where bvm calls it directly, and into share/burn/lib/libburn_runtime.a for native executables, so programs behave the same both ways.

The repository's tests check that the generated standard library, JavaScript runtime and burn doc assets are up to date with their sources.

Packages

A project's burn.toml lists its dependencies, and burn.lock pins each to an exact revision. Installed packages live in ~/.burn/packages (or $BURN_HOME/packages), one folder per package and revision:

~/.burn/packages/github.com/ada/colors@3f2a9c1d0b7e/

The compiler only reads these folders; ash downloads and updates them. A dependency given by path ({ path = "../shared" }) is read from that folder directly. Because the revision is part of the folder name, projects that use different versions of a package never interfere.

The language server

burn lsp (or burn-lsp) is the command line in language server mode. For every change in the editor it runs the front end of the compiler on the open files, with an index of what each name refers to, and answers hovers, completions, go-to-definition, references, renames and diagnostics from it. Because it is the same compiler, the editor shows exactly the errors burn check would. On Linux the server limits its own memory to 4 GB (BURN_LSP_MEMORY_MB changes it, 0 removes the limit), so a runaway analysis stops the server, not the machine.

The REPL

The REPL keeps the code entered so far. Each new input is checked together with it, compiled to a small module and run by bvm with the session's globals kept, so variables, functions and types survive between inputs. :source prints the accumulated code.

How the compiler builds itself

The compiler is a Burn program, so it needs a Burn compiler to build. That compiler is in the repository as a seed: compiler/boot/<platform>.tar.xz holds the compiler from an earlier commit and the runtime it links, compiled to assembly for linux-x86_64, linux-aarch64, macos-x86_64 and macos-aarch64, about 1.3 MB each, the way Zig keeps zig1.wasm. Only a C compiler, tar and xz are needed to start; no Burn has to be downloaded. scripts/bootstrap.sh (which scripts/package.sh runs first):

the seed builds stage 1, stage 1 builds stage 2, and both must be identical

  1. assembles the seed of this platform with the C compiler, giving stage 0, a native compiler;
  2. lets stage 0 compile compiler/src/main.bn and lib/rt to assembly, which the C compiler assembles into stage 1;
  3. lets stage 1 do the same, giving stage 2, and checks that the assembly of stage 1 and stage 2 is the same, byte for byte. When it is not, because the code generator changed since the seed was made, stage 2 builds once more, and stage 2 and stage 3 must be the same.

The last step proves that the compiler, compiled by itself, produces exactly what it was compiled from, so a release does not depend on quirks of the compiler that built it. Each stage takes a few seconds. scripts/package.sh then builds bin/burn and the runtime libraries with the final stage, in under a minute in all. CI runs this on every change.

compiler/src may only use language features that the seed understands. To use a new one in the compiler, the feature lands first; then sh scripts/bootstrap.sh --update replaces the seeds of all four platforms with the compiler just built, and the first use comes with it. Until 26.1.0-experimental-4, a released Burn (stage0) started the bootstrap, and until 26.1.0-experimental-7 a bytecode copy of the compiler that the Rust bvm ran.