How Burn works
Native executables
burn build (and burnc) turns a program into a standalone executable for the machine: x86-64 or 64-bit ARM, on
Linux or macOS. It starts from the same IR as bvm, after the ownership pass, and generates assembly
text that the system's C compiler assembles and links.
IR ──▶ compiler/src/native/x86.bn or arm64.bn ──▶ app.s ──▶ cc ──▶ app
▲
share/burn/lib/libburn_runtime.aburn dump --asm app.bn prints the assembly, and burn build --emit-asm app.s keeps it next to the executable.
A function
fun add(a: int, b: int): int {
return a + b
}becomes, on x86-64:
bf_1:
push rbp
mov rbp, rsp
sub rsp, 32
mov rax, qword ptr [rbp + 24] ; a
mov qword ptr [rbp - 8], rax
mov rax, qword ptr [rbp + 16] ; b
mov qword ptr [rbp - 16], rax
mov rax, qword ptr [rbp - 8]
mov rcx, qword ptr [rbp - 16]
add rax, rcx
jo .L9 ; integer overflow?
mov qword ptr [rbp - 24], rax
mov rax, qword ptr [rbp - 24]
leave
ret
.L9:
mov edi, 1 ; location id of `a + b`
and rsp, -16
call burn_err_overflow@PLT
ud2- Functions are labeled by number (
bf_1);--no-stripkeeps these labels in the symbol table. - Arguments are pushed on the stack by the caller, first argument first, so the last one is closest to the
callee's frame; the caller removes them after the call. Results come back in
rax(x0on ARM). - Locals live in the stack frame at fixed offsets. Expressions are evaluated into
rax/x0, with intermediate values pushed on the stack. - Checks are inline: overflow is a
jo(on ARM, the flags afteradds/subs, or a widening multiply), bounds checks compare with the length stored in the array, null checks test for zero. The error path is placed after the function and calls the runtime with the location id, so the fast path has no extra jumps. - Calls into the runtime (
burn_print,burn_to_str,burn_str_concat, ...) use the platform's C calling convention, with the stack aligned to 16 bytes. - Reference counting is inlined: a retain or release is a null test, a static test, a test of the multi-threaded flag and an increment or decrement of the count at offset 12. Only freeing and the cycle check call into the runtime (memory management).
- Fields and array elements are read with one instruction after the checks, at the offsets described in values in memory.
- Interface calls load the object's type id, index the dispatch table and call through it.
The ARM generator (arm64.bn) produces the same structure with ARM instructions: results in x0, the second operand
in x1, locals relative to the frame pointer x29, addresses formed with adrp/add, constants built with
movz/movk. Both generators share the data layout, the argument order and the decisions about what a program
exports (native/shared.bn), so one runtime library serves both.
Startup
The executable's main is generated too. It passes the runtime everything it needs and then runs the program:
burn_rt_initreceives the program's metadata (its type table and location table, encoded as bytes in the read-only data section), its globals, the address of the stack and the trampoline described below. The runtime builds its type information, installs the stack overflow handler (on Linux x86-64) and marks the main thread.burn_rt_set_argsstores the command line arguments forargs().burn_entryruns the top level code of every module in order, thenmain.burn_rt_exit(0)waits for async tasks, flushes the output and exits.
Globals live in the .bss section, one word each. String literals are complete string objects in the read-only data
section, header included, with the static flag set, so they are never allocated, counted or freed:
bs_1:
.byte 1, 0, 3, 0 ; kind string, flags static and ASCII
.long 6 ; type id: string
.long 28, 0 ; size, reference count
.quad 3 ; length
.byte 104,105,32,0 ; "hi "The trampoline
The runtime sometimes has to call a Burn function of the program: to start an async task, or to run a closure that
was passed to it. Burn functions take their arguments on the stack, which a function with the C calling convention
cannot set up, so every executable contains a short generated function, burn_call_trampoline(f, args, n), that
pushes the n arguments and calls f. The runtime calls the program's code only through it. (Functions marked
@Extern are different: they take their arguments the C way, so C can call them
directly.)
Inline assembly
An asm block has its x86_64 or arm64 code copied into the function as
written. Before it, the code generator stores every operand in a slot of the frame, and {x} becomes that slot's
address (qword ptr [rbp - 24] or [x29, #16]); after it, the slots are copied back into the variables. Nothing in
the generated code keeps a value in a register from one statement to the next, which is why the block may use the
caller-saved registers freely.
Linking
The generated assembly is linked by the C compiler (cc, or BURN_CC/CC) with:
libburn_runtime.a, the runtime written in Burn (lib/rt), compiled to native code, so the executable needs no Burn installation;- the system libraries listed in
native-libs.txtnext to it (the C library, threads, math); - for programs that run bytecode,
libburn_launcher.a, bvm written in Burn (see below).
The libraries are built by scripts/native-runtime.sh with the toolchain's own burn and shipped in
share/burn/lib. Every function is in a section of its own on Linux, so the linker drops the code nothing uses
(--gc-sections). The executable is stripped unless --no-strip is given. Hello world comes out at about 220 KB on
x86-64 Linux.
Bytecode inside native executables
A program that imports bytecode libraries (.bvmc) is compiled to bytecode instead, linked with its libraries and
embedded in an executable together with bvm (libburn_launcher.a, built from compiler/src/bin/launcher.bn), which
runs it at startup. The program therefore behaves exactly as on bvm, mixins included, and its hot code is compiled
by the JIT. burn build app.bar (or .bvm, .bvmc) makes such an executable from
bytecode directly.
Cross-compiling
Code generation only depends on the target platform, which BURN_PLATFORM selects (linux-x86_64,
macos-x86_64, linux-aarch64, macos-aarch64). Linking needs a runtime library and a linker for that platform:
BURN_RUNTIME_DIR and BURN_CC. See cross-compiling.
Stack overflow
The runtime catches a stack overflow with a signal handler running on its own small stack and prints
runtime error: stack overflow (recursion is too deep) before exiting with 1, on Linux and macOS, x86-64 and
arm64.