docs/bytecompiler.md
The bytecompiler is responsible for lowering ECMAScript AST nodes (from boa_ast) into executable bytecode for the virtual machine.
It traverses the parsed AST and emits instructions into a CodeBlock using the ByteCodeEmitter. The resulting bytecode is later executed by the VM.
During compilation, the bytecompiler performs several tasks:
The produced CodeBlock contains the instructions, literal tables, bindings, and handler metadata required for execution.
The flow of the compilation process is as follows:
AST → ByteCompiler → CodeBlock → VM
The bytecompiler performs a lowering step from high-level ECMAScript AST nodes into a lower-level bytecode instruction set understood by the VM.
Each major AST category — such as expression, statement, and declaration - has corresponding compilation logic that emits one or more bytecode instructions.
The compiler operates in a single pass over the AST, generating instructions as it traverses nodes.
Boa uses a register-based bytecode model. Instructions operate on virtual registers instead of stack-based operations.
If you see in here the struct ByteCompiler contains the register allocation field.
pub(crate) register_allocator: RegisterAllocator,
Registers are allocated during the compilation process via RegisterAllocator.
The bytecompiler module is structured according to ECMAScript syntactic categories:
expression/ - compilation of expression nodes (e.g., Binary, Unary)statement/ - compilation of statements and control flow (e.g., If, For, While)declaration/ - handling of variable and function declarations (including patterns)Supporting modules include:
register.rs - register allocation logicjump_control.rs - management of jump targets and control flowfunction.rs - function compilationclass.rs - class compilationmodule.rs - ECMAScript module compilationenv/ - management of environments and scope-related dataThe mod.rs file coordinates the overall compilation process.
The compiler relies on a string interner to store and resolve identifiers efficiently:
pub(crate) interner: &'ctx mut Interner
Identifiers are interned and referenced by symbols (Sym) rather than raw strings. This avoids repeated heap allocations for the same identifier and allows O(1) comparisons by symbol index.
The compiler maintains two separate scopes during AST traversal:
/// The current variable scope.
pub(crate) variable_scope: Scope,
/// The current lexical scope.
pub(crate) lexical_scope: Scope,
variable_scope tracks var declared bindings, which are function-scoped.lexical_scope tracks let and const declared bindings, which are block-scoped.Bindings are resolved using BindingLocator structures, and scope information is embedded into the resulting CodeBlock for runtime environment resolution.
During compilation, the bytecompiler collects values that cannot be embedded directly into instructions - such as strings, numbers, and scope objects - into a constants vector in the CodeBlock.
Instead of embedding these values inline, the compiler stores them in constants and emits the index as the instruction operand. The VM then looks up the value by index at runtime.
For example, when pushing a new scope, the compiler stores it in constants and emits the index:
let index = self.constants.len() as u32;
self.constants.push(Constant::Scope(scope.clone()));
This keeps instructions compact and uniform in size, while still allowing the VM to access arbitrarily complex values.
When compiling control flow constructs like if, while, or for, the compiler needs to emit jump instructions before the jump target is known - because the target code hasn't been compiled yet.
To handle this, the compiler emits the jump with a placeholder address, then comes back and fills in the real address once the target location is known. This process is called jump patching.
The jump_control.rs module manages jump targets, labels, and the bookkeeping required to patch jumps correctly. This includes:
Async functions and generators are not lowered to explicit state machines at the AST level. Instead, the compiler emits handler metadata and suspend/resume points that allow the VM to pause and continue execution across await expressions and yield statements.
The presence of an active async handler is tracked via:
pub(crate) async_handler: Option,
When set, this indicates the index of the handler responsible for managing suspension. The VM uses this information at runtime to correctly propagate values into and out of suspended frames.
The bytecompiler produces CodeBlock structures which contain the instructions and metadata required for execution.
The VM interprets these instructions at runtime. For execution details, see the VM documentation (vm.md).