Bytecode generation
The bridge between the frontend and execution is the
BytecodeGenerator: it walks the AST and emits Ignition
bytecode. Along the way it allocates the feedback slots that
inline caches will fill, and the result is packaged behind a
SharedFunctionInfo. This page connects parsing to running.
::: info Ubiquitous language
SFI (SharedFunctionInfo): the code-agnostic description of a function,
shared by all its closures. BytecodeArray: the emitted bytecode for a
function. Feedback slot: a reserved slot for one operation's runtime feedback.
:::
SharedFunctionInfo
Every function literal has exactly one SFI, shared across all closures created
from it. It holds the function's immutable identity (name, source span, parameter
count) and a polymorphic function_data field that swings between states as the
function is compiled, flushed, and recompiled:
// SharedFunctionInfo describes the JSFunction information that can be
// shared by multiple instances of the function.
V8_OBJECT class SharedFunctionInfo : public HeapObject {
inline Tagged<Object> untrusted_function_data() const;
…
};
— src/objects/shared-function-info.h#L260-L279
function_data can be a BytecodeArray (compiled), UncompiledData with
PreparseData (lazy, not yet
parsed), baseline Code, and more. Note the warning around is_compiled():
// Note: with bytecode flushing, any GC after this call is made could cause the
// function to become uncompiled. … use IsCompiledScope instead.
inline bool is_compiled() const;
— src/objects/shared-function-info.h#L387-L391
::: details Bytecode flushing
Under memory pressure, V8 can discard the bytecode of a function that hasn't
run recently, reverting its SFI to uncompiled; the bytecode is regenerated on next
call. This is why holding an IsCompiledScope matters when you need the bytecode
to stay put. It's a memory/latency trade: spend a recompile to reclaim RAM.
:::
Compilation: a two-phase job
The central Compiler hub dispatches all compilation. Producing bytecode uses an
UnoptimizedCompilationJob split into two phases so the heavy work can run off
the main thread:
// 1) ExecuteJob: Runs concurrently. No heap allocation or handle derefs.
// 2) FinalizeJob: Runs on main thread. No dependency changes.
class UnoptimizedCompilationJob : public CompilationJob {
V8_WARN_UNUSED_RESULT Status ExecuteJob();
V8_WARN_UNUSED_RESULT Status FinalizeJob(
DirectHandle<SharedFunctionInfo>, Isolate*);
};
— src/codegen/compiler.h#L327-L395
Parsing and bytecode generation (ExecuteJob) touch no heap and can run on a
background thread; only the final installation into the SFI (FinalizeJob) is
serialized on the main thread. This keeps main-thread time — the latency users
feel — minimal.
Walking the AST
The generator is an AST visitor that emits bytecode into a builder:
class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
void GenerateBytecode(uintptr_t stack_limit);
template <typename IsolateT>
Handle<BytecodeArray> FinalizeBytecode(IsolateT*, Handle<Script>);
};
— src/interpreter/bytecode-generator.h#L41-L79
Helper builders centralize how each operation is emitted — including allocating its feedback slot:
void BuildLoadNamedProperty(const Expression* object_expr, Register object,
const AstRawString* name);
void BuildLoadKeyedProperty(Register object, FeedbackSlot slot);
— src/interpreter/bytecode-generator.h#L323-L330
This is where the FeedbackVector layout is fixed: the generator decides, deterministically, which operations get feedback slots and in what order. That determinism is essential — the interpreter, baseline code, and the optimizers all index the same slots, so they must agree on the layout.
::: details C++ aside: visitor + scopes
The generator uses the visitor pattern (double dispatch over AST node types) and
a stack of scoped ControlScope helpers to track loops and try/finally, so
break/continue/return resolve to the right targets even when deeply nested.
RAII (constructor pushes, destructor pops) keeps that stack correct under early
exits — a very common V8 idiom.
:::
The handoff
After FinalizeBytecode, the SFI holds a BytecodeArray with feedback metadata,
and the function is ready to run in Ignition. From there,
feedback accumulates and the
tiering machinery takes over.
See also
Scanner, parser & AST — produces the AST consumed here.
Ignition — runs the emitted bytecode.
Inline caches & feedback — fills the slots allocated here.