LibJS is a complete ECMAScript implementation that provides JavaScript parsing, bytecode compilation, and execution capabilities. It serves as the JavaScript engine for the Ladybird browser, handling all JavaScript code execution from parsing source text through bytecode generation to runtime execution.
Scope: This page covers the core JavaScript engine architecture including the parser, bytecode compiler, interpreter, runtime system, and how LibJS is embedded in LibWeb. For information about JavaScript-to-C++ bindings and Web API integration, see page 3 For details about bytecode compilation and interpreter internals, see page 2.1 For the runtime environment, execution contexts, and garbage collector interaction, see page 2.2 For the regular expression engine, see page 2.3
Sources: Libraries/LibJS/Bytecode/Interpreter.cpp1-213 Libraries/LibJS/Bytecode/Generator.cpp1-50 Libraries/LibJS/Runtime/VM.cpp1-50
LibJS follows a multi-stage execution pipeline that transforms JavaScript source code into executable bytecode and then interprets it. The engine is structured around several key subsystems that work together to provide full ECMAScript compliance.
Execution Pipeline
The execution flow begins with source text being parsed into an AST by Parser, which performs lexical and syntactic analysis. The AST is then compiled into bytecode by Bytecode::Generator, producing an Executable containing the instruction stream. The Bytecode::Interpreter executes these instructions within the context of the VM, which manages the heap, execution contexts, and global state.
Sources: Libraries/LibJS/Bytecode/Interpreter.cpp113-213 Libraries/LibJS/Parser.cpp1-50 Libraries/LibJS/Bytecode/Generator.cpp222-273
Core Class Relationships
Sources: Libraries/LibJS/Runtime/VM.h56-159 Libraries/LibJS/Bytecode/Interpreter.h24-108 Libraries/LibJS/Bytecode/Generator.h28-44 Libraries/LibJS/Runtime/ExecutionContext.h59-146
The Parser class (Libraries/LibJS/Parser.cpp) transforms JavaScript source text into an Abstract Syntax Tree (AST). The parser performs lexical analysis, syntax validation, and semantic analysis including scope resolution.
During parsing, the ScopePusher class (Libraries/LibJS/Parser.cpp24-508) tracks lexical scopes, variable declarations, and performs several optimization analyses:
eval() usage and with statements that prevent optimizationsThe parser builds a hierarchy of ScopeNode objects containing declarations at each scope level, which is later used by the bytecode generator to emit appropriate variable access instructions.
| Scope Type | Purpose | Created By |
|---|---|---|
Program | Top-level script or module scope | ScopePusher::program_scope() |
Function | Function body scope | ScopePusher::function_scope() |
Block | Block statement scope | ScopePusher::block_scope() |
Catch | Catch clause scope | ScopePusher::catch_scope() |
With | With statement scope | ScopePusher::with_scope() |
Sources: Libraries/LibJS/Parser.cpp24-508 Libraries/LibJS/AST.h301-356
During scope analysis, the parser classifies each identifier usage to enable bytecode optimization:
Identifier Optimization Classification
The parser tracks three main categories of identifier access patterns:
Sources: Libraries/LibJS/Parser.cpp298-435 Libraries/LibJS/AST.h531-566
The Bytecode::Generator class (Libraries/LibJS/Bytecode/Generator.cpp) transforms the AST into a stream of bytecode instructions. This compilation phase performs optimizations like constant folding, register allocation, and dead code elimination.
The bytecode uses a typed operand system with four categories stored in a contiguous layout:
[Registers | Locals | Constants | Arguments]
| Operand Type | Purpose | Allocation |
|---|---|---|
Register | Temporary values during computation | Generator::allocate_register() |
Local | Function-local variables (optimized var/let/const) | Pre-allocated per function |
Constant | Compile-time constant values | Generator::add_constant() |
Argument | Function parameters | Pre-allocated per call |
Sources: Libraries/LibJS/Bytecode/Generator.cpp310-349 Libraries/LibJS/Runtime/ExecutionContext.h112-145
Bytecode Generation Flow
Each AST node implements generate_bytecode() (Libraries/LibJS/Bytecode/ASTCodegen.cpp) which recursively generates bytecode for its children and emits instructions into the current BasicBlock. The generator maintains a current block pointer and creates new blocks for control flow constructs.
Sources: Libraries/LibJS/Bytecode/ASTCodegen.cpp34-473 Libraries/LibJS/Bytecode/Generator.cpp222-487
The Generator::emit<OpType>() template method (Libraries/LibJS/Bytecode/Generator.h88-122) handles instruction emission:
BasicBlockInstructions are defined declaratively in Libraries/LibJS/Bytecode/Bytecode.def and code-generated by Meta/generate-libjs-bytecode-def-derived.py
Sources: Libraries/LibJS/Bytecode/Generator.h88-122 Libraries/LibJS/Bytecode/Bytecode.def1-50
The generator performs compile-time constant folding for binary and unary operations when both operands are constants:
This optimization eliminates runtime computation for expressions like 2 + 3 or "hello" + " world", replacing them with their constant results.
Sources: Libraries/LibJS/Bytecode/ASTCodegen.cpp85-163 Libraries/LibJS/Bytecode/ASTCodegen.cpp315-348
The Bytecode::Interpreter class (Libraries/LibJS/Bytecode/Interpreter.cpp) executes bytecode instructions using a computed goto dispatch mechanism for performance.
Bytecode Interpreter Dispatch Loop
The interpreter uses a single dispatch loop (Libraries/LibJS/Bytecode/Interpreter.cpp257-636) with computed goto labels for each instruction type. This avoids the overhead of a switch statement and provides better branch prediction.
Sources: Libraries/LibJS/Bytecode/Interpreter.cpp257-636 Libraries/LibJS/Bytecode/Interpreter.cpp231-255
The interpreter accesses operands through the ExecutionContext which maintains all values in a contiguous array:
The Operand stores a raw index that directly indexes into this array after the operand layout transformation performed during compilation.
Sources: Libraries/LibJS/Bytecode/Interpreter.cpp94-102 Libraries/LibJS/Bytecode/Operand.h1-50
The bytecode supports structured exception handling with try/catch/finally blocks:
Exception Handler Resolution
Exception handlers are registered per basic block and form a tree structure. The handle_exception() method (Libraries/LibJS/Bytecode/Interpreter.cpp231-255) searches for the nearest handler by PC offset.
Sources: Libraries/LibJS/Bytecode/Interpreter.cpp231-255 Libraries/LibJS/Bytecode/Generator.cpp469-487
The runtime system manages execution state, memory allocation, and provides the ECMAScript object model.
The VM class (Libraries/LibJS/Runtime/VM.cpp) is the central coordinator that owns:
Each JavaScript function call creates an ExecutionContext (Libraries/LibJS/Runtime/ExecutionContext.h59-146) allocated on the native stack:
This allocates space for the execution context header plus a variable-sized tail array containing all operand values in the layout: [registers | locals | constants | arguments].
Sources: Libraries/LibJS/Runtime/VM.h56-159 Libraries/LibJS/Runtime/ExecutionContext.h59-146 Libraries/LibJS/Runtime/ExecutionContext.cpp21-100
LibJS implements two primary function object types:
| Class | Purpose | Creation |
|---|---|---|
ECMAScriptFunctionObject | User-defined functions | Compiled from AST |
NativeFunction | Built-in functions | C++ callback wrapper |
Function Object Hierarchy
ECMAScriptFunctionObject (Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp) represents functions defined in JavaScript source code. Multiple function instances can share the same SharedFunctionInstanceData when created from the same function definition.
Sources: Libraries/LibJS/Runtime/FunctionObject.h20-65 Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h28-141 Libraries/LibJS/Runtime/NativeFunction.h19-67
The environment system implements ECMAScript's lexical scoping:
Environment Record Types
this binding and new.target supportwith)let/const) and object (var) recordsSources: Libraries/LibJS/Runtime/DeclarativeEnvironment.h Libraries/LibJS/Runtime/FunctionEnvironment.h Libraries/LibJS/Runtime/GlobalEnvironment.h
Function calls in LibJS follow a well-defined sequence that sets up execution state, runs bytecode, and handles return values.
Function Call Sequence
The call sequence demonstrates several important aspects:
alloca, avoiding heap allocation overheadprepare_for_ordinary_call() creates a new FunctionEnvironment for the function's local scopeReturn or End instructionSources: Libraries/LibJS/Runtime/AbstractOperations.cpp52-99 Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp272-314 Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp446-482
Constructor calls differ from regular calls in their handling of this and return values:
Constructor Execution Flow
Key differences from regular calls:
this object before executionthis uninitialized until super() is calledthis, primitives are ignoredthis binding but before user codeSources: Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp317-400 Libraries/LibJS/Runtime/AbstractOperations.cpp101-124
ECMAScript functions lazily compile their bytecode on first execution. get_stack_frame_size() (Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp216-229) triggers this compilation:
shared_data().m_executable is null, it calls Bytecode::compile() to produce a GC::Ref<Bytecode::Executable>ecmascript_code() body directlyBytecode::Generator::generate_from_function() using the SharedFunctionInstanceDataclear_compile_inputs() releases the AST to reduce memory usageExecutable is cached in SharedFunctionInstanceData so multiple closure instances sharing the same function definition share one bytecode objectSources: Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp216-229 Libraries/LibJS/Bytecode/Interpreter.cpp622-644
LibJS aims to implement the ECMAScript specification faithfully. Every non-trivial operation in the codebase is annotated with a link to the corresponding spec section, and abstract operations are named to match the spec text directly.
For example, Interpreter::run() (Libraries/LibJS/Bytecode/Interpreter.cpp113-201) opens with the comment // 16.1.6 ScriptEvaluation ( scriptRecord ). Function call setup in ECMAScriptFunctionObject::prepare_for_ordinary_call() (Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp406-450) cites section 10.2.1.1, and each numbered step from the spec is preserved as a comment.
The abstract operations in Libraries/LibJS/Runtime/AbstractOperations.cpp and Libraries/LibJS/Runtime/AbstractOperations.h mirror the spec's abstract operations — require_object_coercible, call_impl, construct_impl, length_of_array_like, species_constructor, validate_and_apply_property_descriptor, and others — each cited to their spec section.
Where the implementation diverges from the spec (for optimization or implementation convenience), the deviation is called out with a // FIXME: or // NOTE: comment.
LibJS is embedded in LibWeb, which hosts one VM instance per browser process. At startup, VM::create() (Libraries/LibJS/Runtime/VM.cpp42-63) initializes the VM and its well-known symbols. LibWeb then overrides the VM's host hooks — callable slots on the VM struct — to integrate JavaScript with the browser's event loop and platform services:
| Host Hook | Default | LibWeb Override |
|---|---|---|
host_enqueue_promise_job | Runs jobs inline | Enqueues on HTML microtask queue |
host_promise_rejection_tracker | Internal tracking | Fires unhandledrejection events |
host_load_imported_module | Stub | Initiates network fetch for module |
host_ensure_can_compile_strings | Always allow | Enforces Content Security Policy |
host_get_import_meta_properties | Returns {} | Returns module URL etc. |
Each browsing context gets its own Realm with a GlobalObject, and the LibWeb Window object is set as the global object for script execution. JavaScript objects exposed to scripts are created through the WebIDL bindings system (see page 3).
Sources: Libraries/LibJS/Runtime/VM.cpp99-173 Libraries/LibJS/Runtime/VM.h56-200
All JavaScript values are represented by the Value class (Libraries/LibJS/Runtime/Value.h), which uses NaN-boxing to fit all types into a single 64-bit word. A valid IEEE 754 double has specific bit patterns; any value with the upper 13 bits set in a particular way is a "canonical NaN." LibJS encodes non-double types into these otherwise-unused bit patterns.
The tag constants (Libraries/LibJS/Runtime/Value.h37-56) define the encoding:
| Tag | Type | Notes |
|---|---|---|
OBJECT_TAG | Pointer to GC::Cell | Upper bits indicate GC-managed pointer |
STRING_TAG | Pointer to PrimitiveString | GC-managed |
SYMBOL_TAG | Pointer to Symbol | GC-managed |
BIGINT_TAG | Pointer to BigInt | GC-managed |
ACCESSOR_TAG | Pointer to Accessor | GC-managed |
UNDEFINED_TAG | undefined | Inline, no pointer |
NULL_TAG | null | Inline, no pointer |
BOOLEAN_TAG | true/false | Inline, no pointer |
INT32_TAG | 32-bit integer | Inline fast path for common integers |
EMPTY_TAG | Internal sentinel | Array holes, uninitialized registers |
| (no tag) | double | Any IEEE 754 double that isn't a tagged NaN |
The fast paths throughout the interpreter take advantage of this — comparisons first check tag == tag then fall back to the full is_loosely_equal for the mixed-type case (Libraries/LibJS/Bytecode/Interpreter.cpp55-89).
PrimitiveString (Libraries/LibJS/Runtime/PrimitiveString.cpp) objects are interned in the VM. Strings up to 256 characters are de-duplicated via a cache on the VM (m_string_cache, m_utf16_string_cache). Single ASCII character strings are pre-allocated at VM startup. This avoids repeated allocation and enables pointer equality for common strings.
Sources: Libraries/LibJS/Runtime/Value.h32-70 Libraries/LibJS/Runtime/PrimitiveString.cpp24-60 Libraries/LibJS/Runtime/VM.cpp73-98
LibJS bytecode consists of variable-length instructions defined in Libraries/LibJS/Bytecode/Bytecode.def Each instruction type has specific operands and semantics.
| Category | Instructions | Purpose |
|---|---|---|
| Data Movement | Mov, Load | Copy values between operands |
| Arithmetic | Add, Sub, Mul, Div, Mod, Exp | Mathematical operations |
| Comparison | LessThan, GreaterThan, StrictlyEquals, LooselyEquals | Comparison operations with fast paths |
| Bitwise | BitwiseAnd, BitwiseOr, BitwiseXor, LeftShift | Bit manipulation |
| Control Flow | Jump, JumpIf, JumpTrue, JumpFalse | Conditional and unconditional jumps |
| Function Calls | Call, CallConstruct, CallWithArgumentArray | Function invocation |
| Property Access | GetById, GetByValue, PutById, PutByValue | Object property operations |
| Environment | CreateLexicalEnvironment, GetBinding, SetBinding | Scope management |
| Exception Handling | Throw, EnterUnwindContext, LeaveUnwindContext | Try/catch/finally support |
| Terminator | Return, End, Yield, Await | Control flow termination |
Property access instructions use polymorphic inline caches to optimize repeated lookups:
Property Lookup Cache Operation
The cache stores up to 4 shape entries (Libraries/LibJS/Bytecode/Executable.h30-63). On a cache miss, the slow path performs full property resolution and updates the cache. Subsequent accesses to objects with the same shape hit the fast path, avoiding hash lookups and prototype walks.
Sources: Libraries/LibJS/Bytecode/PropertyAccess.h62-400 Libraries/LibJS/Bytecode/Executable.h30-87
The interpreter achieves high performance through computed goto:
This technique:
Refresh this wiki