ladybird/Libraries/LibJS/Rust/src/bytecode/generator.rs
Andreas Kling 4ac744082b LibJS: Cache dynamic environment coordinates
Dynamic environment binding opcodes lost the old coordinate warmup.
They were split away from the static coordinate opcodes. Hot closures
and eval-sensitive functions then resolved the same binding by name on
every execution, which regressed JS benchmark throughput badly.

Give each dynamic environment opcode a per-executable coordinate cache
slot. The cache keeps the bytecode stream immutable while letting both
interpreters take a direct declarative environment fast path after the
first lookup. Keep the existing eval invalidation behavior and only warm
caches for declarative-only chains so with environments continue to
observe object shadowing.

Reject cached bytecode that uses the no-cache sentinel for dynamic
environment coordinate cache operands, since execution indexes those
cache arrays unconditionally.

Rebaseline bytecode expectations for the instruction size changes. Add
coverage for with-object shadowing across repeated dynamic lookups and
for rejecting corrupt dynamic environment cache indices.
2026-05-19 15:54:23 +02:00

2067 lines
82 KiB
Rust

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
//! Bytecode generator.
//!
//! This module contains the `Generator` struct which manages all state
//! needed for bytecode generation from the AST.
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use super::basic_block::{BasicBlock, SourceMapEntry};
use super::ffi::{AbstractOperationKind, WellKnownSymbolKind};
use super::instruction::Instruction;
use super::operand::*;
use crate::ast::{AstArena, FunctionData, FunctionId, FunctionTable, IdentifierId, LocalType, Position, Utf16String};
use crate::u32_from_usize;
use std::sync::Arc;
/// Identifies an operand that auto-frees its register when the last
/// clone is dropped.
///
/// Wraps `Rc<ScopedOperandInner>`. When the last `Rc` clone drops
/// and the operand is a non-reserved register, the `Drop` impl
/// returns it to the generator's register pool for reuse.
#[derive(Debug, Clone)]
pub struct ScopedOperand {
pub(crate) inner: std::rc::Rc<ScopedOperandInner>,
}
pub(crate) struct ScopedOperandInner {
operand: Operand,
free_register_pool: Rc<RefCell<Vec<Register>>>,
}
pub struct PendingSharedFunctionData {
pub function_data: Option<Box<FunctionData>>,
pub subtable: Option<FunctionTable>,
pub arena: Option<Arc<AstArena>>,
pub name_override: Option<Utf16String>,
pub class_field_initializer_name: Option<(Utf16String, bool)>,
pub should_eager_compile: bool,
pub precompiled_function: Option<Box<PrecompiledFunction>>,
}
/// Metadata computed from scope analysis for a SharedFunctionInstanceData.
pub struct FunctionSfdMetadata {
pub uses_this: bool,
pub this_value_needs_environment_resolution: bool,
pub function_environment_needed: bool,
pub function_environment_bindings_count: usize,
pub var_environment_bindings_count: usize,
pub might_need_arguments: bool,
pub contains_eval: bool,
}
/// GC-free compiled bytecode for a function that top-level code will immediately invoke.
pub struct PrecompiledFunction {
pub generator: Box<Generator>,
pub assembled: AssembledBytecode,
pub metadata: FunctionSfdMetadata,
}
#[derive(Clone, Copy)]
pub enum PendingLiteralValueKind {
None,
Number,
BooleanTrue,
BooleanFalse,
Null,
String,
}
pub struct PendingClassElement {
pub kind: u8,
pub is_static: bool,
pub is_private: bool,
pub private_identifier: Option<Utf16String>,
pub shared_function_data_index: Option<u32>,
pub has_initializer: bool,
pub literal_value_kind: PendingLiteralValueKind,
pub literal_value_number: f64,
pub literal_value_string: Option<Utf16String>,
}
pub struct PendingClassBlueprint {
pub name: Option<Utf16String>,
pub source_text_offset: usize,
pub source_text_length: usize,
pub constructor_sfd_index: u32,
pub has_super_class: bool,
pub has_name: bool,
pub elements: Vec<PendingClassElement>,
}
struct EnvironmentCoordinateScope {
bindings: HashMap<Utf16String, u32>,
next_binding_index: u32,
kind: EnvironmentCoordinateScopeKind,
}
#[derive(PartialEq)]
enum EnvironmentCoordinateScopeKind {
// A declarative environment whose bindings are created by bytecode we emit.
// The binding indexes are therefore known while generating the instruction
// stream and can be embedded in EnvironmentCoordinate operands.
Static,
// An object environment, such as `with`, can intercept any name. Once one
// is between the current point and a binding, resolution must stay dynamic.
Dynamic,
}
const ENVIRONMENT_MODE_LEXICAL: u32 = 0;
impl std::fmt::Debug for ScopedOperandInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ScopedOperandInner({:?})", self.operand)
}
}
impl Drop for ScopedOperandInner {
fn drop(&mut self) {
if self.operand.is_register() && self.operand.index() >= Register::RESERVED_COUNT {
self.free_register_pool
.borrow_mut()
.push(Register(self.operand.index()));
}
}
}
impl ScopedOperand {
pub fn operand(&self) -> Operand {
self.inner.operand
}
}
impl PartialEq for ScopedOperand {
fn eq(&self, other: &Self) -> bool {
self.inner.operand == other.inner.operand
}
}
pub use crate::ast::FunctionKind;
/// Block boundary types for unwind tracking.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockBoundaryType {
Break,
Continue,
ReturnToFinally,
LeaveFinally,
LeaveLexicalEnvironment,
}
/// A break/continue scope with its target label and language labels.
pub struct LabelableScope {
pub bytecode_target: Label,
pub language_label_set: Vec<Utf16String>,
pub completion_register: Option<ScopedOperand>,
}
/// Codegen-time state for a try/finally scope.
///
/// Stored in `Generator::finally_contexts` Vec, referenced by index.
/// This avoids the deep-clone issues of an owned `Box` parent chain.
pub struct FinallyContext {
pub completion_type: ScopedOperand,
pub completion_value: ScopedOperand,
pub finally_body: Label,
pub exception_preamble: Label,
pub parent_index: Option<usize>,
pub registered_jumps: Vec<FinallyJump>,
pub next_jump_index: i32,
pub lexical_environment_at_entry: Option<ScopedOperand>,
pub saved_unwind_handler: Option<Label>,
}
impl FinallyContext {
pub const NORMAL: i32 = 0;
pub const THROW: i32 = 1;
pub const RETURN: i32 = 2;
pub const FIRST_JUMP_INDEX: i32 = 3;
}
/// A break/continue target registered with a FinallyContext.
pub struct FinallyJump {
pub index: i32,
pub target: Label,
}
/// A local variable name with metadata.
#[derive(Debug)]
pub struct LocalVariable {
pub name: Utf16String,
pub is_lexically_declared: bool,
pub is_initialized_during_declaration_instantiation: bool,
}
/// The bytecode generator.
///
/// Manages all state needed for compiling an AST into bytecode.
pub struct Generator {
// --- Basic block management ---
pub basic_blocks: Vec<BasicBlock>,
current_block_index: Label,
// --- Register allocation ---
next_register: u32,
free_register_pool: Rc<RefCell<Vec<Register>>>,
// --- Constant pool ---
pub constants: Vec<ConstantValue>,
// Cached constants for deduplication
true_constant: Option<ScopedOperand>,
false_constant: Option<ScopedOperand>,
null_constant: Option<ScopedOperand>,
undefined_constant: Option<ScopedOperand>,
empty_constant: Option<ScopedOperand>,
int32_constants: HashMap<i32, ScopedOperand>,
double_constants: HashMap<u64, ScopedOperand>,
string_constants: HashMap<Utf16String, ScopedOperand>,
// --- String/identifier/property tables (with deduplication) ---
pub string_table: Vec<Utf16String>,
string_table_index: HashMap<Utf16String, StringTableIndex>,
pub identifier_table: Vec<Utf16String>,
identifier_table_index: HashMap<Utf16String, IdentifierTableIndex>,
pub property_key_table: Vec<Utf16String>,
property_key_table_index: HashMap<Utf16String, PropertyKeyTableIndex>,
pub compiled_regexes: Vec<*mut std::ffi::c_void>,
// --- Scope/unwind state ---
pub boundaries: Vec<BlockBoundaryType>,
pub continuable_scopes: Vec<LabelableScope>,
pub breakable_scopes: Vec<LabelableScope>,
pub pending_labels: Vec<Utf16String>,
pub lexical_environment_register_stack: Vec<ScopedOperand>,
// Mirrors lexical_environment_register_stack for environments whose binding
// layout is known to codegen. This lets us emit immutable coordinates
// instead of runtime-updated caches in common lexical lookup instructions.
environment_coordinate_scope_stack: Vec<EnvironmentCoordinateScope>,
// `var` binding instructions start from vm.variable_environment(), not the
// current lexical environment. Keep a separate anchor so a var write inside
// a nested block does not accidentally count the block as a hop.
variable_environment_coordinate_scope_index: Option<usize>,
pub home_objects: Vec<ScopedOperand>,
// --- Finally context ---
// FinallyContext objects are stored in this Vec and referenced by index.
// This avoids the deep-clone issues of an owned Box parent chain.
pub finally_contexts: Vec<FinallyContext>,
pub current_finally_context: Option<usize>,
// --- Various counters ---
pub next_property_lookup_cache: u32,
pub next_global_variable_cache: u32,
pub next_environment_coordinate_cache: u32,
pub next_template_object_cache: u32,
pub next_object_shape_cache: u32,
pub next_object_property_iterator_cache: u32,
// --- Codegen state ---
pub strict: bool,
pub this_value_needs_environment_resolution: bool,
pub enclosing_function_kind: FunctionKind,
pub local_variables: Vec<LocalVariable>,
pub initialized_locals: Vec<bool>,
pub initialized_arguments: Vec<bool>,
/// When set, function/class expressions will use this as their `.name`.
/// Set by assignment/declaration codegen, consumed by function expression codegen.
pub pending_lhs_name: Option<IdentifierTableIndex>,
// Source location tracking
pub current_source_start: Position,
pub current_source_end: Position,
// --- Completion register ---
pub current_completion_register: Option<ScopedOperand>,
pub must_propagate_completion: bool,
// --- Accumulator and this ---
accumulator: ScopedOperand,
this_value: ScopedOperand,
// --- Shared function data ---
// Pending descriptors for SharedFunctionInstanceData objects. These are
// materialized at the C++ boundary so bytecode generation can run without
// allocating GC cells.
pub shared_function_data: Vec<PendingSharedFunctionData>,
pub eager_compile_function_ids: HashSet<FunctionId>,
pub eager_compile_direct_iifes: bool,
// --- Class blueprints ---
// Pending descriptors for ClassBlueprint objects. Ownership transfers to
// the Executable after materialization.
pub class_blueprints: Vec<PendingClassBlueprint>,
// --- Length identifier cache ---
pub length_identifier: Option<PropertyKeyTableIndex>,
// --- Unwind context ---
// When set, newly created basic blocks inherit this handler index.
pub current_unwind_handler: Option<Label>,
// --- AnnexB function names ---
// Names approved for AnnexB.3.3 hoisting by the scope collector.
// Populated during FDI, checked in switch case codegen.
pub annexb_function_names: HashSet<Utf16String>,
// --- Builtin abstract operations ---
// When true, calls to known abstract operations (e.g. IsCallable, GetMethod)
// are compiled to specialized bytecode instructions rather than normal calls.
// Used for builtin JS files.
pub builtin_abstract_operations_enabled: bool,
// --- FFI context ---
// These are set by the top-level compiler and passed through for
// creating SharedFunctionInstanceData via FFI callbacks.
pub vm_ptr: *mut std::ffi::c_void,
pub source_code_ptr: *const std::ffi::c_void,
pub source_len: usize,
// --- Function table ---
// Side table owning all FunctionData from the parser. Codegen
// takes ownership of individual entries via `take()`.
pub function_table: crate::ast::FunctionTable,
// --- AST arena ---
// Shared (read-only post-parse) storage for identifiers, scopes, and
// interned strings. Cloning is a refcount bump — multiple generators
// (top-level + nested IIFE + lazy children) share the same arena.
pub arena: Arc<AstArena>,
}
impl Generator {
/// Convenience: look up an identifier by ID in this generator's arena.
#[inline]
pub fn identifier(&self, id: IdentifierId) -> &crate::ast::Identifier {
&self.arena.identifiers[id]
}
}
macro_rules! singleton_constant {
($self:expr_2021, $field:ident, $value:expr_2021) => {{
if let Some(op) = &$self.$field {
return op.clone();
}
let op = $self.append_constant($value);
$self.$field = Some(op.clone());
op
}};
}
macro_rules! next_cache_method {
($method:ident, $field:ident) => {
pub fn $method(&mut self) -> u32 {
let index = self.$field;
self.$field += 1;
index
}
};
}
macro_rules! define_intern_method {
($method_name:ident, $index_type:ident, $table:ident, $cache:ident) => {
pub fn $method_name(&mut self, s: &[u16]) -> $index_type {
if let Some(&index) = self.$cache.get(s) {
return index;
}
let index = $index_type(u32_from_usize(self.$table.len()));
let key = Utf16String(s.to_vec());
self.$table.push(key.clone());
self.$cache.insert(key, index);
index
}
};
}
impl Default for Generator {
fn default() -> Self {
Self::new()
}
}
impl Generator {
/// Create a new bytecode generator.
pub fn new() -> Self {
let free_register_pool = Rc::new(RefCell::new(Vec::new()));
Self {
basic_blocks: Vec::new(),
current_block_index: Label(0),
next_register: Register::RESERVED_COUNT,
constants: Vec::new(),
true_constant: None,
false_constant: None,
null_constant: None,
undefined_constant: None,
empty_constant: None,
int32_constants: HashMap::new(),
double_constants: HashMap::new(),
string_constants: HashMap::new(),
string_table: Vec::new(),
string_table_index: HashMap::new(),
identifier_table: Vec::new(),
identifier_table_index: HashMap::new(),
property_key_table: Vec::new(),
property_key_table_index: HashMap::new(),
compiled_regexes: Vec::new(),
boundaries: Vec::new(),
continuable_scopes: Vec::new(),
breakable_scopes: Vec::new(),
pending_labels: Vec::new(),
lexical_environment_register_stack: Vec::new(),
environment_coordinate_scope_stack: Vec::new(),
variable_environment_coordinate_scope_index: None,
home_objects: Vec::new(),
finally_contexts: Vec::new(),
current_finally_context: None,
next_property_lookup_cache: 0,
next_global_variable_cache: 0,
next_environment_coordinate_cache: 0,
next_template_object_cache: 0,
next_object_shape_cache: 0,
next_object_property_iterator_cache: 0,
strict: false,
this_value_needs_environment_resolution: true,
enclosing_function_kind: FunctionKind::Normal,
local_variables: Vec::new(),
initialized_locals: Vec::new(),
initialized_arguments: Vec::new(),
pending_lhs_name: None,
current_source_start: Position {
line: 0,
column: 0,
offset: 0,
},
current_source_end: Position {
line: 0,
column: 0,
offset: 0,
},
current_completion_register: None,
must_propagate_completion: false,
accumulator: ScopedOperand {
inner: Rc::new(ScopedOperandInner {
operand: Operand::register(Register::ACCUMULATOR),
free_register_pool: free_register_pool.clone(),
}),
},
this_value: ScopedOperand {
inner: Rc::new(ScopedOperandInner {
operand: Operand::register(Register::THIS_VALUE),
free_register_pool: free_register_pool.clone(),
}),
},
shared_function_data: Vec::new(),
eager_compile_function_ids: HashSet::new(),
eager_compile_direct_iifes: false,
class_blueprints: Vec::new(),
length_identifier: None,
current_unwind_handler: None,
annexb_function_names: HashSet::new(),
builtin_abstract_operations_enabled: false,
vm_ptr: std::ptr::null_mut(),
source_code_ptr: std::ptr::null(),
source_len: 0,
function_table: crate::ast::FunctionTable::new(),
arena: Arc::new(AstArena::new()),
free_register_pool,
}
}
// --- Function kind queries ---
pub fn is_in_generator_function(&self) -> bool {
matches!(
self.enclosing_function_kind,
FunctionKind::Generator | FunctionKind::AsyncGenerator
)
}
pub fn is_in_async_function(&self) -> bool {
matches!(
self.enclosing_function_kind,
FunctionKind::Async | FunctionKind::AsyncGenerator
)
}
pub fn is_in_async_generator_function(&self) -> bool {
self.enclosing_function_kind == FunctionKind::AsyncGenerator
}
pub fn is_in_generator_or_async_function(&self) -> bool {
self.enclosing_function_kind != FunctionKind::Normal
}
pub fn is_in_finalizer(&self) -> bool {
self.boundaries.contains(&BlockBoundaryType::LeaveFinally)
}
// --- Register management ---
/// Allocate a new register (or reuse a freed one).
pub fn allocate_register(&mut self) -> ScopedOperand {
let reg = {
let mut pool = self.free_register_pool.borrow_mut();
match pool.pop() {
Some(r) => r,
None => {
let r = Register(self.next_register);
self.next_register += 1;
r
}
}
};
self.scoped_operand(Operand::register(reg))
}
/// Get a ScopedOperand for a local variable.
pub fn local(&mut self, index: u32) -> ScopedOperand {
self.scoped_operand(Operand::local(index))
}
/// Resolve a local binding (argument or variable) to a ScopedOperand.
pub fn resolve_local(&mut self, index: u32, local_type: LocalType) -> ScopedOperand {
match local_type {
LocalType::Argument => self.scoped_operand(Operand::argument(index)),
LocalType::Variable => self.local(index),
}
}
/// Get the accumulator register.
pub fn accumulator(&self) -> ScopedOperand {
self.accumulator.clone()
}
/// Get the this_value register.
pub fn this_value(&self) -> ScopedOperand {
self.this_value.clone()
}
/// Get the exception register as a raw Operand (not ScopedOperand since
/// it's a fixed register that should not be freed).
pub fn exception_operand(&self) -> Operand {
Operand::register(Register::EXCEPTION)
}
/// Copy a local variable into a fresh register to prevent later
/// side effects from changing its value. Returns the operand unchanged
/// if it is not a local.
pub fn copy_if_needed_to_preserve_evaluation_order(&mut self, operand: &ScopedOperand) -> ScopedOperand {
match operand.operand().operand_type() {
OperandType::Register | OperandType::Constant => operand.clone(),
OperandType::Local | OperandType::Argument => {
let reg = self.allocate_register();
self.emit_mov(&reg, operand);
reg
}
}
}
pub fn scoped_operand(&mut self, operand: Operand) -> ScopedOperand {
ScopedOperand {
inner: Rc::new(ScopedOperandInner {
operand,
free_register_pool: self.free_register_pool.clone(),
}),
}
}
// --- Constant pool ---
fn append_constant(&mut self, value: ConstantValue) -> ScopedOperand {
let index = u32_from_usize(self.constants.len());
self.constants.push(value);
self.scoped_operand(Operand::constant(index))
}
pub fn add_constant_number(&mut self, value: f64) -> ScopedOperand {
// Deduplicate i32 values (but not -0.0, which has distinct semantics from +0.0)
if value.fract() == 0.0
&& value >= i32::MIN as f64
&& value <= i32::MAX as f64
&& value.to_bits() != (-0.0_f64).to_bits()
{
let as_i32 = value as i32;
if let Some(op) = self.int32_constants.get(&as_i32) {
return op.clone();
}
let op = self.append_constant(ConstantValue::Number(value));
self.int32_constants.insert(as_i32, op.clone());
return op;
}
// Deduplicate double values by their bit representation
let as_bits = value.to_bits();
if let Some(op) = self.double_constants.get(&as_bits) {
return op.clone();
}
let op = self.append_constant(ConstantValue::Number(value));
self.double_constants.insert(as_bits, op.clone());
op
}
pub fn add_constant_boolean(&mut self, value: bool) -> ScopedOperand {
if value {
singleton_constant!(self, true_constant, ConstantValue::Boolean(true))
} else {
singleton_constant!(self, false_constant, ConstantValue::Boolean(false))
}
}
pub fn add_constant_null(&mut self) -> ScopedOperand {
singleton_constant!(self, null_constant, ConstantValue::Null)
}
pub fn add_constant_undefined(&mut self) -> ScopedOperand {
singleton_constant!(self, undefined_constant, ConstantValue::Undefined)
}
pub fn add_constant_empty(&mut self) -> ScopedOperand {
singleton_constant!(self, empty_constant, ConstantValue::Empty)
}
pub fn add_constant_string(&mut self, value: Utf16String) -> ScopedOperand {
if let Some(op) = self.string_constants.get(&value) {
return op.clone();
}
let op = self.append_constant(ConstantValue::String(value.clone()));
self.string_constants.insert(value, op.clone());
op
}
pub fn add_constant_bigint(&mut self, value: String) -> ScopedOperand {
self.append_constant(ConstantValue::BigInt(value))
}
pub fn add_constant_well_known_symbol(&mut self, symbol: WellKnownSymbolKind) -> ScopedOperand {
self.append_constant(ConstantValue::WellKnownSymbol(symbol))
}
pub fn add_constant_abstract_operation(&mut self, operation: AbstractOperationKind) -> ScopedOperand {
self.append_constant(ConstantValue::AbstractOperation(operation))
}
/// Get the constant value for a constant operand.
pub fn get_constant(&self, operand: &ScopedOperand) -> Option<&ConstantValue> {
if operand.operand().is_constant() {
self.constants.get(operand.operand().index() as usize)
} else {
None
}
}
// --- Table interning ---
define_intern_method!(intern_string, StringTableIndex, string_table, string_table_index);
define_intern_method!(
intern_identifier,
IdentifierTableIndex,
identifier_table,
identifier_table_index
);
define_intern_method!(
intern_property_key,
PropertyKeyTableIndex,
property_key_table,
property_key_table_index
);
/// Convenience: look up a `StringId` in the AST interner and intern the
/// resulting slice into the bytecode identifier table.
pub fn intern_identifier_id(&mut self, id: crate::ast::StringId) -> IdentifierTableIndex {
let arena = self.arena.clone();
let slice = arena.strings[id].as_slice();
self.intern_identifier(slice)
}
/// Convenience: look up a `StringId` in the AST interner and intern the
/// resulting slice into the bytecode property key table.
pub fn intern_property_key_id(&mut self, id: crate::ast::StringId) -> PropertyKeyTableIndex {
let arena = self.arena.clone();
let slice = arena.strings[id].as_slice();
self.intern_property_key(slice)
}
/// Convenience: look up a `StringId` in the AST interner and intern the
/// resulting slice into the bytecode string table.
pub fn intern_string_id(&mut self, id: crate::ast::StringId) -> StringTableIndex {
let arena = self.arena.clone();
let slice = arena.strings[id].as_slice();
self.intern_string(slice)
}
/// If `operand` is a constant string that is not an array index, intern it
/// as a property key and return the index. Uses split borrows to avoid
/// cloning the string when it is already interned (the common case).
pub fn try_constant_string_to_property_key(&mut self, operand: &ScopedOperand) -> Option<PropertyKeyTableIndex> {
if !operand.operand().is_constant() {
return None;
}
let idx = operand.operand().index() as usize;
let s: &[u16] = match self.constants.get(idx) {
Some(ConstantValue::String(s)) if !super::codegen::is_array_index(&s.0) => &s.0,
_ => return None,
};
// Split borrow: s borrows self.constants, get() borrows self.property_key_table_index
if let Some(&key_index) = self.property_key_table_index.get(s) {
return Some(key_index);
}
// Cold path: not yet interned, must clone
let owned = Utf16String(s.to_vec());
let key_index = PropertyKeyTableIndex(u32_from_usize(self.property_key_table.len()));
self.property_key_table.push(owned.clone());
self.property_key_table_index.insert(owned, key_index);
Some(key_index)
}
/// Register a pending SharedFunctionInstanceData descriptor and return its index.
pub fn register_shared_function_data(&mut self, data: PendingSharedFunctionData) -> u32 {
let index = u32_from_usize(self.shared_function_data.len());
self.shared_function_data.push(data);
index
}
pub fn set_class_field_initializer_name(&mut self, index: u32, name: Utf16String, is_private: bool) {
if let Some(data) = self.shared_function_data.get_mut(index as usize) {
data.class_field_initializer_name = Some((name, is_private));
}
}
/// Register a pending ClassBlueprint descriptor and return its index.
pub fn register_class_blueprint(&mut self, data: PendingClassBlueprint) -> u32 {
let index = u32_from_usize(self.class_blueprints.len());
self.class_blueprints.push(data);
index
}
pub fn intern_regex(&mut self, compiled: *mut std::ffi::c_void) -> RegexTableIndex {
let index = u32_from_usize(self.compiled_regexes.len());
self.compiled_regexes.push(compiled);
RegexTableIndex(index)
}
// --- Basic block management ---
/// Create a new basic block and return its label.
pub fn make_block(&mut self) -> Label {
let index = self.basic_blocks.len();
let mut block = BasicBlock::new(u32_from_usize(index));
// Propagate exception handler from active unwind context.
if let Some(handler) = self.current_unwind_handler {
block.handler = Some(handler);
}
self.basic_blocks.push(block);
Label(u32_from_usize(index))
}
/// Switch emission to the given basic block.
pub fn switch_to_basic_block(&mut self, label: Label) {
self.current_block_index = label;
}
/// Get the current basic block's label.
pub fn current_block_index(&self) -> Label {
self.current_block_index
}
/// Is the current block terminated?
pub fn is_current_block_terminated(&self) -> bool {
self.basic_blocks[self.current_block_index.basic_block_index()].terminated
}
/// Number of basic blocks.
pub fn basic_block_count(&self) -> usize {
self.basic_blocks.len()
}
/// Terminate all unterminated blocks with Yield (no continuation).
/// Used for generator and async functions.
pub fn terminate_unterminated_blocks_with_yield(&mut self) {
let block_count = self.basic_block_count();
for i in 0..block_count {
let label = Label(u32_from_usize(i));
if self.is_block_terminated(label) {
continue;
}
self.switch_to_basic_block(label);
let undef = self.add_constant_undefined();
self.emit(Instruction::Yield {
continuation_label: None,
value: undef.operand(),
});
}
}
/// Is a specific block terminated?
pub fn is_block_terminated(&self, label: Label) -> bool {
self.basic_blocks[label.basic_block_index()].terminated
}
// --- Instruction emission ---
/// Emit an instruction to the current basic block.
pub fn emit(&mut self, instruction: Instruction) {
if self.is_current_block_terminated() {
return;
}
// Keep coordinate scopes in lockstep with the actual declarative
// environment shape. Most bindings are created explicitly, while
// CreateArguments can implicitly create an `arguments` binding.
if let Instruction::CreateVariable {
identifier,
mode,
is_global,
..
} = &instruction
&& !is_global
{
let name = self.identifier_table[identifier.0 as usize].clone();
if *mode == ENVIRONMENT_MODE_LEXICAL {
self.record_environment_binding(name);
} else {
self.record_variable_environment_binding(name);
}
}
if let Instruction::CreateMutableBinding { identifier, .. }
| Instruction::CreateImmutableBinding { identifier, .. } = &instruction
{
let name = self.identifier_table[identifier.0 as usize].clone();
self.record_environment_binding(name);
}
if let Instruction::CreateArguments { dst: None, .. } = &instruction {
self.record_environment_binding(Utf16String::from(utf16!("arguments")));
}
let source_map = SourceMapEntry {
bytecode_offset: 0, // filled during flattening
line: self.current_source_start.line,
column: self.current_source_start.column,
};
let block = &mut self.basic_blocks[self.current_block_index.basic_block_index()];
block.append(instruction, source_map);
}
/// Emit a Mov instruction (optimized away if src == dst).
pub fn emit_mov(&mut self, dst: &ScopedOperand, src: &ScopedOperand) {
if dst != src {
self.emit(Instruction::Mov {
dst: dst.operand(),
src: src.operand(),
});
}
}
pub fn emit_mov_raw(&mut self, dst: Operand, src: Operand) {
// NB: Unlike emit_mov (ScopedOperand version), this does NOT skip
// self-moves and emits unconditionally.
self.emit(Instruction::Mov { dst, src });
}
/// Emit a conditional jump, with comparison fusion and constant folding.
pub fn emit_jump_if(&mut self, condition: &ScopedOperand, true_target: Label, false_target: Label) {
// OPTIMIZATION: If condition is a constant, emit an unconditional jump.
if let Some(constant) = self.get_constant(condition)
&& let Some(is_truthy) = constant_to_boolean(constant)
{
self.emit(Instruction::Jump {
target: if is_truthy { true_target } else { false_target },
});
return;
}
// OPTIMIZATION: If the condition is a register with ref_count == 1 and the last
// instruction is a comparison whose dst matches condition, fuse into a JumpXxx.
if condition.operand().is_register() && std::rc::Rc::strong_count(&condition.inner) == 1 {
let block = &mut self.basic_blocks[self.current_block_index.basic_block_index()];
if let Some((last_instruction, _)) = block.instructions.last() {
let fused = match last_instruction {
Instruction::LessThan { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpLessThan {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::LessThanEquals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpLessThanEquals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::GreaterThan { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpGreaterThan {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::GreaterThanEquals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpGreaterThanEquals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::LooselyEquals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpLooselyEquals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::LooselyInequals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpLooselyInequals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::StrictlyEquals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpStrictlyEquals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
Instruction::StrictlyInequals { dst, lhs, rhs } if *dst == condition.operand() => {
Some(Instruction::JumpStrictlyInequals {
lhs: *lhs,
rhs: *rhs,
true_target,
false_target,
})
}
_ => None,
};
if let Some(fused_instruction) = fused {
// Remove the comparison instruction and emit the fused jump.
block.instructions.pop();
self.emit(fused_instruction);
return;
}
}
}
self.emit(Instruction::JumpIf {
condition: condition.operand(),
true_target,
false_target,
});
}
// --- Cache index allocation ---
next_cache_method!(next_property_lookup_cache, next_property_lookup_cache);
next_cache_method!(next_global_variable_cache, next_global_variable_cache);
next_cache_method!(next_environment_coordinate_cache, next_environment_coordinate_cache);
next_cache_method!(next_template_object_cache, next_template_object_cache);
next_cache_method!(next_object_shape_cache, next_object_shape_cache);
next_cache_method!(next_object_property_iterator_cache, next_object_property_iterator_cache);
// --- Lexical environment helpers ---
pub fn current_lexical_environment(&mut self) -> ScopedOperand {
self.lexical_environment_register_stack
.last()
.cloned()
.unwrap_or_else(|| self.scoped_operand(Operand::register(Register::SAVED_LEXICAL_ENVIRONMENT)))
}
pub fn capture_saved_lexical_environment(&mut self) {
let env_reg = self.scoped_operand(Operand::register(Register::SAVED_LEXICAL_ENVIRONMENT));
self.emit(Instruction::GetLexicalEnvironment { dst: env_reg.operand() });
self.push_untracked_lexical_environment(env_reg);
}
pub fn capture_saved_lexical_environment_with_coordinates(&mut self) {
let env_reg = self.scoped_operand(Operand::register(Register::SAVED_LEXICAL_ENVIRONMENT));
self.emit(Instruction::GetLexicalEnvironment { dst: env_reg.operand() });
self.push_static_lexical_environment(env_reg);
self.variable_environment_coordinate_scope_index = self.environment_coordinate_scope_stack.len().checked_sub(1);
}
pub fn end_variable_scope(&mut self) {
self.end_boundary(BlockBoundaryType::LeaveLexicalEnvironment);
self.pop_tracked_lexical_environment();
if !self.is_current_block_terminated() {
let parent = self.current_lexical_environment();
self.emit(Instruction::SetLexicalEnvironment {
environment: parent.operand(),
});
}
}
pub fn allocate_completion_register(&mut self) -> Option<ScopedOperand> {
if self.must_propagate_completion {
let reg = self.allocate_register();
let undef = self.add_constant_undefined();
self.emit_mov(&reg, &undef);
Some(reg)
} else {
None
}
}
pub fn push_new_lexical_environment(&mut self, capacity: u32) -> ScopedOperand {
self.push_new_lexical_environment_impl(capacity, false)
}
pub fn push_new_catch_lexical_environment(&mut self, capacity: u32) -> ScopedOperand {
self.push_new_lexical_environment_impl(capacity, true)
}
fn push_new_lexical_environment_impl(&mut self, capacity: u32, is_catch_environment: bool) -> ScopedOperand {
let parent = self.current_lexical_environment();
let new_env = self.allocate_register();
self.emit(Instruction::CreateLexicalEnvironment {
dst: new_env.operand(),
parent: parent.operand(),
capacity,
is_catch_environment,
});
self.push_static_lexical_environment(new_env.clone());
new_env
}
pub fn push_untracked_lexical_environment(&mut self, environment: ScopedOperand) {
self.lexical_environment_register_stack.push(environment);
}
pub fn push_static_lexical_environment(&mut self, environment: ScopedOperand) {
self.push_untracked_lexical_environment(environment);
self.push_environment_coordinate_scope(EnvironmentCoordinateScopeKind::Static);
}
pub fn push_dynamic_lexical_environment(&mut self, environment: ScopedOperand) {
self.push_untracked_lexical_environment(environment);
self.push_environment_coordinate_scope(EnvironmentCoordinateScopeKind::Dynamic);
}
pub fn push_static_variable_environment(&mut self, environment: ScopedOperand) {
self.push_static_lexical_environment(environment);
self.variable_environment_coordinate_scope_index = self.environment_coordinate_scope_stack.len().checked_sub(1);
}
pub fn pop_untracked_lexical_environment(&mut self) -> Option<ScopedOperand> {
self.lexical_environment_register_stack.pop()
}
pub fn pop_tracked_lexical_environment(&mut self) -> Option<ScopedOperand> {
self.pop_environment_coordinate_scope();
self.pop_untracked_lexical_environment()
}
fn push_environment_coordinate_scope(&mut self, kind: EnvironmentCoordinateScopeKind) {
self.environment_coordinate_scope_stack
.push(EnvironmentCoordinateScope {
bindings: HashMap::new(),
next_binding_index: 0,
kind,
});
}
fn pop_environment_coordinate_scope(&mut self) {
self.environment_coordinate_scope_stack.pop();
}
fn record_environment_binding(&mut self, name: Utf16String) {
let Some(scope_index) = self.environment_coordinate_scope_stack.len().checked_sub(1) else {
return;
};
self.record_environment_binding_at_scope_index(name, scope_index);
}
fn record_variable_environment_binding(&mut self, name: Utf16String) {
let Some(scope_index) = self.variable_environment_coordinate_scope_index else {
return;
};
self.record_environment_binding_at_scope_index(name, scope_index);
}
fn record_environment_binding_at_scope_index(&mut self, name: Utf16String, scope_index: usize) {
let Some(scope) = self.environment_coordinate_scope_stack.get_mut(scope_index) else {
return;
};
if scope.kind == EnvironmentCoordinateScopeKind::Dynamic {
return;
}
// DeclarativeEnvironment appends duplicate bindings and resolves the
// name to the newest slot, so mirror that layout here.
scope.bindings.insert(name, scope.next_binding_index);
scope.next_binding_index += 1;
}
pub fn environment_coordinate_for(&self, name: &[u16]) -> Option<EnvironmentCoordinate> {
self.environment_coordinate_for_from_scope_index(
name,
self.environment_coordinate_scope_stack.len().checked_sub(1)?,
)
}
fn environment_coordinate_for_from_scope_index(
&self,
name: &[u16],
scope_index: usize,
) -> Option<EnvironmentCoordinate> {
// Coordinates are only safe through fully-known declarative scopes. If
// any dynamic scope is crossed, preserve the runtime lookup semantics.
for (hops, scope) in self.environment_coordinate_scope_stack[..=scope_index]
.iter()
.rev()
.enumerate()
{
if scope.kind == EnvironmentCoordinateScopeKind::Dynamic {
return None;
}
if let Some(index) = scope.bindings.get(name) {
return Some(EnvironmentCoordinate {
hops: u32_from_usize(hops),
index: *index,
});
}
}
None
}
pub fn environment_coordinate_for_identifier(
&self,
identifier: IdentifierTableIndex,
) -> Option<EnvironmentCoordinate> {
let name = &self.identifier_table[identifier.0 as usize];
self.environment_coordinate_for(name)
}
pub fn variable_environment_coordinate_for_identifier(
&self,
identifier: IdentifierTableIndex,
) -> Option<EnvironmentCoordinate> {
let name = &self.identifier_table[identifier.0 as usize];
self.environment_coordinate_for_from_scope_index(name, self.variable_environment_coordinate_scope_index?)
}
// --- Boundary management ---
pub fn start_boundary(&mut self, ty: BlockBoundaryType) {
self.boundaries.push(ty);
}
pub fn end_boundary(&mut self, ty: BlockBoundaryType) {
assert_eq!(self.boundaries.last(), Some(&ty));
self.boundaries.pop();
}
// --- Break/continue scope management ---
pub fn begin_breakable_scope(
&mut self,
target: Label,
label_set: Vec<Utf16String>,
completion: Option<ScopedOperand>,
) {
self.breakable_scopes.push(LabelableScope {
bytecode_target: target,
language_label_set: label_set,
completion_register: completion,
});
self.start_boundary(BlockBoundaryType::Break);
}
pub fn end_breakable_scope(&mut self) {
self.end_boundary(BlockBoundaryType::Break);
self.breakable_scopes.pop();
}
pub fn begin_continuable_scope(
&mut self,
target: Label,
label_set: Vec<Utf16String>,
completion: Option<ScopedOperand>,
) {
self.continuable_scopes.push(LabelableScope {
bytecode_target: target,
language_label_set: label_set,
completion_register: completion,
});
self.start_boundary(BlockBoundaryType::Continue);
}
pub fn end_continuable_scope(&mut self) {
self.end_boundary(BlockBoundaryType::Continue);
self.continuable_scopes.pop();
}
pub fn set_current_breakable_scope_completion_register(&mut self, completion: ScopedOperand) {
self.breakable_scopes
.last_mut()
.expect("no active breakable scope")
.completion_register = Some(completion);
}
pub fn find_breakable_scope(&self, label: Option<&[u16]>) -> Option<&LabelableScope> {
if let Some(label) = label {
self.breakable_scopes
.iter()
.rev()
.find(|s| s.language_label_set.iter().any(|l| l == label))
} else {
self.breakable_scopes.last()
}
}
pub fn find_continuable_scope(&self, label: Option<&[u16]>) -> Option<&LabelableScope> {
if let Some(label) = label {
self.continuable_scopes
.iter()
.rev()
.find(|s| s.language_label_set.iter().any(|l| l == label))
} else {
self.continuable_scopes.last()
}
}
// --- FinallyContext support ---
/// Push a new FinallyContext and set it as current. Returns its index.
pub fn push_finally_context(&mut self, mut ctx: FinallyContext) -> usize {
let index = self.finally_contexts.len();
ctx.saved_unwind_handler = self.current_unwind_handler;
self.finally_contexts.push(ctx);
self.current_finally_context = Some(index);
index
}
/// Check if there is an outer ReturnToFinally boundary between `boundary_index`
/// and the matching break/continue boundary.
fn has_outer_finally_before_target(&self, is_break: bool, boundary_index: usize) -> bool {
for j in (0..boundary_index.saturating_sub(1)).rev() {
let inner = self.boundaries[j];
if (is_break && inner == BlockBoundaryType::Break) || (!is_break && inner == BlockBoundaryType::Continue) {
return false;
}
if inner == BlockBoundaryType::ReturnToFinally {
return true;
}
}
false
}
/// Register a jump target with the current FinallyContext.
/// Assigns a unique completion_type index and emits code to set it and jump to finally.
pub fn register_jump_in_finally_context(&mut self, target: Label) {
let index = self.current_finally_context.expect("no active finally context");
let ctx = &mut self.finally_contexts[index];
let jump_index = ctx.next_jump_index;
ctx.next_jump_index += 1;
ctx.registered_jumps.push(FinallyJump {
index: jump_index,
target,
});
let completion_type = ctx.completion_type.clone();
let finally_body = ctx.finally_body;
let index_const = self.add_constant_i32(jump_index);
self.emit_mov(&completion_type, &index_const);
self.emit(Instruction::Jump { target: finally_body });
}
/// For break/continue through nested finally: create a trampoline block.
fn emit_trampoline_through_finally(&mut self) {
let trampoline_block = self.make_block();
self.register_jump_in_finally_context(trampoline_block);
self.switch_to_basic_block(trampoline_block);
// Pop to the parent FinallyContext (simulating the inner finally completing).
let index = self.current_finally_context.expect("no active finally context");
self.current_unwind_handler = self.finally_contexts[index].saved_unwind_handler;
self.current_finally_context = self.finally_contexts[index].parent_index;
}
/// Generate a break, walking boundaries and handling FinallyContext.
pub fn generate_break(&mut self, label: Option<&[u16]>) {
if let Some(label) = label {
self.generate_labelled_jump(true, label);
} else {
self.generate_scoped_jump(true);
}
}
/// Generate a continue, walking boundaries and handling FinallyContext.
pub fn generate_continue(&mut self, label: Option<&[u16]>) {
if let Some(label) = label {
self.generate_labelled_jump(false, label);
} else {
self.generate_scoped_jump(false);
}
}
/// Walk boundaries for unlabelled break/continue.
fn generate_scoped_jump(&mut self, is_break: bool) {
let saved_ctx = self.current_finally_context;
let env_stack_len = self.lexical_environment_register_stack.len();
let mut env_offset = env_stack_len;
let mut i = self.boundaries.len();
while i > 0 {
i -= 1;
let boundary = self.boundaries[i];
match boundary {
BlockBoundaryType::Break if is_break => {
let target_scope = self.breakable_scopes.last().expect("no active breakable scope");
let target = target_scope.bytecode_target;
let completion = target_scope.completion_register.clone();
if let (Some(cur), Some(tgt)) = (self.current_completion_register.clone(), completion)
&& cur != tgt
{
self.emit_mov(&tgt, &cur);
}
self.emit(Instruction::Jump { target });
self.current_finally_context = saved_ctx;
return;
}
BlockBoundaryType::Continue if !is_break => {
let target_scope = self.continuable_scopes.last().expect("no active continuable scope");
let target = target_scope.bytecode_target;
let completion = target_scope.completion_register.clone();
if let (Some(cur), Some(tgt)) = (self.current_completion_register.clone(), completion)
&& cur != tgt
{
self.emit_mov(&tgt, &cur);
}
self.emit(Instruction::Jump { target });
self.current_finally_context = saved_ctx;
return;
}
BlockBoundaryType::LeaveLexicalEnvironment => {
env_offset -= 1;
let env = self.lexical_environment_register_stack[env_offset - 1].clone();
self.emit(Instruction::SetLexicalEnvironment {
environment: env.operand(),
});
}
BlockBoundaryType::ReturnToFinally => {
if !self.has_outer_finally_before_target(is_break, i + 1) {
let target_scope = if is_break {
self.breakable_scopes.last().expect("no active breakable scope")
} else {
self.continuable_scopes.last().expect("no active continuable scope")
};
let target = target_scope.bytecode_target;
let completion = target_scope.completion_register.clone();
if let (Some(cur), Some(tgt)) = (self.current_completion_register.clone(), completion)
&& cur != tgt
{
self.emit_mov(&tgt, &cur);
}
self.register_jump_in_finally_context(target);
self.current_finally_context = saved_ctx;
return;
}
self.emit_trampoline_through_finally();
}
_ => {}
}
}
self.current_finally_context = saved_ctx;
}
/// Walk boundaries for labelled break/continue.
fn generate_labelled_jump(&mut self, is_break: bool, label: &[u16]) {
let saved_ctx = self.current_finally_context;
let env_stack_len = self.lexical_environment_register_stack.len();
let mut env_offset = env_stack_len;
let jumpable_scopes: Vec<(Label, Vec<Utf16String>, Option<ScopedOperand>)> = if is_break {
self.breakable_scopes
.iter()
.rev()
.map(|s| {
(
s.bytecode_target,
s.language_label_set.clone(),
s.completion_register.clone(),
)
})
.collect()
} else {
self.continuable_scopes
.iter()
.rev()
.map(|s| {
(
s.bytecode_target,
s.language_label_set.clone(),
s.completion_register.clone(),
)
})
.collect()
};
let mut current_boundary = self.boundaries.len();
for (target, label_set, completion) in &jumpable_scopes {
while current_boundary > 0 {
current_boundary -= 1;
let boundary = self.boundaries[current_boundary];
match boundary {
BlockBoundaryType::LeaveLexicalEnvironment => {
env_offset -= 1;
let env = self.lexical_environment_register_stack[env_offset - 1].clone();
self.emit(Instruction::SetLexicalEnvironment {
environment: env.operand(),
});
}
BlockBoundaryType::ReturnToFinally => {
if !self.has_outer_finally_before_target(is_break, current_boundary + 1)
&& label_set.iter().any(|l| l == label)
{
if let (Some(cur), Some(tgt)) =
(self.current_completion_register.clone(), completion.clone())
&& cur != tgt
{
self.emit_mov(&tgt, &cur);
}
self.register_jump_in_finally_context(*target);
self.current_finally_context = saved_ctx;
return;
}
self.emit_trampoline_through_finally();
}
b if (is_break && b == BlockBoundaryType::Break)
|| (!is_break && b == BlockBoundaryType::Continue) =>
{
break;
}
_ => {}
}
}
if label_set.iter().any(|l| l == label) {
if let (Some(cur), Some(tgt)) = (self.current_completion_register.clone(), completion.clone())
&& cur != tgt
{
self.emit_mov(&tgt, &cur);
}
self.emit(Instruction::Jump { target: *target });
self.current_finally_context = saved_ctx;
return;
}
}
self.current_finally_context = saved_ctx;
}
/// Walk the boundary stack and emit SetLexicalEnvironment instructions
/// for each LeaveLexicalEnvironment boundary, restoring the parent
/// environment. Stops at ReturnToFinally since the finally handler
/// takes care of further unwinding.
pub fn perform_needed_unwinds(&mut self) {
let mut env_stack_offset = self.lexical_environment_register_stack.len();
for i in (0..self.boundaries.len()).rev() {
match self.boundaries[i] {
BlockBoundaryType::LeaveLexicalEnvironment => {
env_stack_offset -= 1;
let parent_env = self.lexical_environment_register_stack[env_stack_offset - 1].clone();
self.emit(Instruction::SetLexicalEnvironment {
environment: parent_env.operand(),
});
}
BlockBoundaryType::ReturnToFinally => {
return;
}
_ => {}
}
}
}
/// Generate a return, routing through FinallyContext if needed.
pub fn generate_return(&mut self, value: &ScopedOperand) {
self.perform_needed_unwinds();
if let Some(index) = self.current_finally_context {
let ctx = &self.finally_contexts[index];
let completion_value = ctx.completion_value.clone();
let completion_type = ctx.completion_type.clone();
let finally_body = ctx.finally_body;
self.emit_mov(&completion_value, value);
let ret_const = self.add_constant_i32(FinallyContext::RETURN);
self.emit_mov(&completion_type, &ret_const);
self.emit(Instruction::Jump { target: finally_body });
} else if self.is_in_generator_or_async_function() {
self.emit(Instruction::Yield {
continuation_label: None,
value: value.operand(),
});
} else {
self.emit(Instruction::Return { value: value.operand() });
}
}
pub fn add_constant_i32(&mut self, val: i32) -> ScopedOperand {
self.add_constant_number(val as f64)
}
// --- Local variable initialization tracking ---
pub fn is_local_initialized(&self, index: u32) -> bool {
self.initialized_locals.get(index as usize).copied().unwrap_or(false)
}
pub fn is_local_lexically_declared(&self, index: u32) -> bool {
self.local_variables
.get(index as usize)
.is_some_and(|v| v.is_lexically_declared)
}
pub fn mark_local_initialized(&mut self, index: u32) {
let index = index as usize;
if index >= self.initialized_locals.len() {
self.initialized_locals.resize(index + 1, false);
}
self.initialized_locals[index] = true;
}
pub fn is_argument_initialized(&self, index: u32) -> bool {
self.initialized_arguments.get(index as usize).copied().unwrap_or(false)
}
pub fn mark_argument_initialized(&mut self, index: u32) {
let index = index as usize;
if index >= self.initialized_arguments.len() {
self.initialized_arguments.resize(index + 1, false);
}
self.initialized_arguments[index] = true;
}
// --- Compile/assemble/link pipeline ---
/// Compile all basic blocks into a flat bytecode buffer.
///
/// This performs:
/// 1. Operand rewriting (offset indices for the runtime layout)
/// 2. Compute block byte offsets using encoded_size()
/// 3. Patch labels in typed instructions (block index → byte offset)
/// 4. Encode to bytes and build source map + exception handlers
pub fn assemble(&mut self) -> AssembledBytecode {
let saved_environment = Operand::register(Register::SAVED_LEXICAL_ENVIRONMENT);
let synthetic_load_block = self.basic_blocks.iter().position(|block| {
matches!(
block.instructions.first(),
Some((
Instruction::GetLexicalEnvironment {
dst,
},
_
)) if *dst == saved_environment
)
});
if let Some(load_block_index) = synthetic_load_block {
let saved_environment_is_used = self.basic_blocks.iter().enumerate().any(|(block_index, block)| {
block
.instructions
.iter()
.enumerate()
.any(|(instruction_index, (instruction, _))| {
if block_index == load_block_index && instruction_index == 0 {
return false;
}
let mut instruction = instruction.clone();
let mut mentions_saved_environment = false;
instruction.visit_operands(&mut |operand: &mut Operand| {
if *operand == saved_environment {
mentions_saved_environment = true;
}
});
mentions_saved_environment
})
});
if !saved_environment_is_used {
self.basic_blocks[load_block_index].instructions.remove(0);
}
}
// If any block is unterminated, ensure the undefined constant exists
// for the assembly-time End(undefined) fallthrough. This must happen
// before computing number_of_constants so operand rewriting accounts
// for it.
let has_unterminated = self.basic_blocks.iter().any(|b| !b.terminated);
let undefined_constant_operand = if has_unterminated {
Some(self.add_constant_undefined().operand())
} else {
None
};
let number_of_registers = self.next_register;
let number_of_locals = u32_from_usize(self.local_variables.len());
let number_of_constants = u32_from_usize(self.constants.len());
// Phase 1: Operand rewriting
let mut max_argument_index: Option<u32> = None;
for block in &mut self.basic_blocks {
for (instruction, _) in &mut block.instructions {
instruction.visit_operands(&mut |op: &mut Operand| {
match op.operand_type() {
OperandType::Register => {} // stays as-is
OperandType::Local => op.offset_index_by(number_of_registers),
OperandType::Constant => {
op.offset_index_by(number_of_registers + number_of_locals);
}
OperandType::Argument => {
let index = op.index();
max_argument_index = Some(max_argument_index.map_or(index, |m| m.max(index)));
op.offset_index_by(number_of_registers + number_of_locals + number_of_constants);
}
}
});
}
}
let number_of_arguments = max_argument_index.map_or(0, |m| m + 1);
// Phase 1b: Peephole optimization - merge consecutive Mov instructions into Mov2/Mov3.
for block in &mut self.basic_blocks {
let mut i = 0;
while i < block.instructions.len() {
if !matches!(block.instructions[i].0, Instruction::Mov { .. }) {
i += 1;
continue;
}
let (dst1, src1) = match &block.instructions[i].0 {
Instruction::Mov { dst, src } => (*dst, *src),
_ => unreachable!(),
};
// Check for a second consecutive Mov.
if i + 1 < block.instructions.len()
&& let Instruction::Mov { dst, src } = &block.instructions[i + 1].0
{
let (dst2, src2) = (*dst, *src);
// Identical Movs: deduplicate to a single Mov.
if dst1 == dst2 && src1 == src2 {
block.instructions.remove(i + 1);
continue; // Re-check from same position.
}
// Check for a third consecutive Mov.
if i + 2 < block.instructions.len()
&& let Instruction::Mov { dst, src } = &block.instructions[i + 2].0
{
let (dst3, src3) = (*dst, *src);
let mov2_is_dup = dst2 == dst1 && src2 == src1;
let mov3_is_dup = (dst3 == dst1 && src3 == src1) || (dst3 == dst2 && src3 == src2);
if mov2_is_dup && mov3_is_dup {
// All three identical: keep single Mov.
block.instructions.remove(i + 2);
block.instructions.remove(i + 1);
continue;
} else if mov2_is_dup {
// mov1 == mov2, mov3 different: Mov2(mov1, mov3).
block.instructions[i].0 = Instruction::Mov2 {
dst1,
src1,
dst2: dst3,
src2: src3,
};
block.instructions.remove(i + 2);
block.instructions.remove(i + 1);
i += 1;
continue;
} else if mov3_is_dup {
// mov3 is dup: Mov2(mov1, mov2).
block.instructions[i].0 = Instruction::Mov2 { dst1, src1, dst2, src2 };
block.instructions.remove(i + 2);
block.instructions.remove(i + 1);
i += 1;
continue;
} else {
// All three unique: Mov3.
block.instructions[i].0 = Instruction::Mov3 {
dst1,
src1,
dst2,
src2,
dst3,
src3,
};
block.instructions.remove(i + 2);
block.instructions.remove(i + 1);
i += 1;
continue;
}
}
// Only two unique Movs: Mov2.
block.instructions[i].0 = Instruction::Mov2 { dst1, src1, dst2, src2 };
block.instructions.remove(i + 1);
i += 1;
continue;
}
i += 1;
}
}
// Phase 2: Compute block byte offsets, applying assembly-time optimizations:
// - Skip Jump-to-next-block
// - Replace Jump-to-Return/End-only-block with inline Return/End
// - Replace JumpIf-where-one-target-is-next-block with JumpTrue/JumpFalse
let num_blocks = self.basic_blocks.len();
let mut block_offsets: Vec<usize> = Vec::with_capacity(num_blocks);
// Per-instruction skip flags: skip_flags[block_index][instruction_index] = replacement action
#[derive(Clone, Copy)]
enum InstAction {
Emit,
Skip,
JumpToReturn(Operand),
JumpToEnd(Operand),
EmitJumpTrue { condition: Operand, target: Label },
EmitJumpFalse { condition: Operand, target: Label },
}
let mut actions: Vec<Vec<InstAction>> = Vec::with_capacity(num_blocks);
let mut offset: usize = 0;
for block_index in 0..num_blocks {
block_offsets.push(offset);
let block = &self.basic_blocks[block_index];
let mut block_actions = Vec::with_capacity(block.instructions.len());
for (instruction, _) in &block.instructions {
match instruction {
Instruction::Jump { target } => {
let target_block = target.0 as usize;
// OPTIMIZATION: Don't emit jumps that just jump to the next block.
if target_block == block_index + 1 {
// If this block would become empty, we handle it by
// not advancing offset.
block_actions.push(InstAction::Skip);
continue;
}
// OPTIMIZATION: For jumps to a return-or-end-only block, inline
// the Return/End instead of emitting the Jump.
let target_blk = &self.basic_blocks[target_block];
if target_blk.terminated && target_blk.instructions.len() == 1 {
match &target_blk.instructions[0].0 {
Instruction::Return { value } => {
let replacement = Instruction::Return { value: *value };
block_actions.push(InstAction::JumpToReturn(*value));
offset += replacement.encoded_size();
continue;
}
Instruction::End { value } => {
let replacement = Instruction::End { value: *value };
block_actions.push(InstAction::JumpToEnd(*value));
offset += replacement.encoded_size();
continue;
}
_ => {}
}
}
block_actions.push(InstAction::Emit);
offset += instruction.encoded_size();
}
Instruction::JumpIf {
condition,
true_target,
false_target,
} => {
let true_block = true_target.0 as usize;
let false_block = false_target.0 as usize;
// OPTIMIZATION: Replace JumpIf where one target is next block
// with JumpTrue or JumpFalse.
if true_block == block_index + 1 {
block_actions.push(InstAction::EmitJumpFalse {
condition: *condition,
target: *false_target,
});
let replacement = Instruction::JumpFalse {
condition: *condition,
target: *false_target,
};
offset += replacement.encoded_size();
continue;
}
if false_block == block_index + 1 {
block_actions.push(InstAction::EmitJumpTrue {
condition: *condition,
target: *true_target,
});
let replacement = Instruction::JumpTrue {
condition: *condition,
target: *true_target,
};
offset += replacement.encoded_size();
continue;
}
block_actions.push(InstAction::Emit);
offset += instruction.encoded_size();
}
_ => {
block_actions.push(InstAction::Emit);
offset += instruction.encoded_size();
}
}
}
// Unterminated blocks get an implicit End(undefined) appended.
if !block.terminated {
let dummy_end = Instruction::End {
value: Operand::constant(0),
};
offset += dummy_end.encoded_size();
}
actions.push(block_actions);
}
// NB: Empty blocks (from skipped jumps) have the same byte offset as
// the next block, so labels referencing them resolve correctly.
// Phase 3: Patch labels (block index → byte offset)
for block in &mut self.basic_blocks {
for (instruction, _) in &mut block.instructions {
instruction.visit_labels(&mut |label: &mut Label| {
let block_index = label.0 as usize;
label.0 = u32_from_usize(block_offsets[block_index]);
});
}
}
// Phase 4: Encode to bytes with optimizations applied
let mut bytecode: Vec<u8> = Vec::with_capacity(offset);
let mut source_map: Vec<SourceMapEntry> = Vec::new();
let mut exception_handlers: Vec<ExceptionHandler> = Vec::new();
fn push_source_map_entry(source_map: &mut Vec<SourceMapEntry>, entry: SourceMapEntry) {
let should_push = source_map
.last()
.is_none_or(|previous| previous.line != entry.line || previous.column != entry.column);
if should_push {
source_map.push(entry);
}
}
// Track which blocks actually produced instructions.
let mut basic_block_start_offsets: Vec<usize> = Vec::with_capacity(num_blocks);
for (block_index, block) in self.basic_blocks.iter().enumerate() {
basic_block_start_offsets.push(bytecode.len());
let block_start = bytecode.len();
let handler = block.handler;
let block_actions = &actions[block_index];
for (instruction_index, (instruction, sm)) in block.instructions.iter().enumerate() {
let action = block_actions[instruction_index];
match action {
InstAction::Skip => {
// If this skip makes the block empty, remove it from
// basic_block_start_offsets.
if basic_block_start_offsets.last() == Some(&bytecode.len()) {
basic_block_start_offsets.pop();
}
}
InstAction::Emit => {
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: sm.line,
column: sm.column,
},
);
instruction.encode(self.strict, &mut bytecode);
}
InstAction::JumpToReturn(value) => {
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: sm.line,
column: sm.column,
},
);
let replacement = Instruction::Return { value };
replacement.encode(self.strict, &mut bytecode);
}
InstAction::JumpToEnd(value) => {
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: sm.line,
column: sm.column,
},
);
let replacement = Instruction::End { value };
replacement.encode(self.strict, &mut bytecode);
}
InstAction::EmitJumpFalse { condition, mut target } => {
// Patch label for the target
let target_block = target.0 as usize;
target.0 = u32_from_usize(block_offsets[target_block]);
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: sm.line,
column: sm.column,
},
);
let replacement = Instruction::JumpFalse { condition, target };
replacement.encode(self.strict, &mut bytecode);
}
InstAction::EmitJumpTrue { condition, mut target } => {
let target_block = target.0 as usize;
target.0 = u32_from_usize(block_offsets[target_block]);
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: sm.line,
column: sm.column,
},
);
let replacement = Instruction::JumpTrue { condition, target };
replacement.encode(self.strict, &mut bytecode);
}
}
}
// Unterminated blocks get an implicit End(undefined).
if !block.terminated {
let mut undef_rewritten = undefined_constant_operand.expect("undefined constant must exist");
undef_rewritten.offset_index_by(number_of_registers + number_of_locals);
let end_instruction = Instruction::End { value: undef_rewritten };
let instruction_offset = bytecode.len();
push_source_map_entry(
&mut source_map,
SourceMapEntry {
bytecode_offset: u32_from_usize(instruction_offset),
line: 0,
column: 0,
},
);
end_instruction.encode(self.strict, &mut bytecode);
}
// Close exception handler range
if let Some(handler_label) = handler {
exception_handlers.push(ExceptionHandler {
start_offset: u32_from_usize(block_start),
end_offset: u32_from_usize(bytecode.len()),
handler_offset: u32_from_usize(block_offsets[handler_label.basic_block_index()]),
});
}
}
// Merge adjacent exception handlers with the same handler offset.
let mut merged_handlers: Vec<ExceptionHandler> = Vec::new();
for handler in &exception_handlers {
if let Some(last) = merged_handlers.last_mut()
&& last.end_offset == handler.start_offset
&& last.handler_offset == handler.handler_offset
{
last.end_offset = handler.end_offset;
continue;
}
merged_handlers.push(handler.clone());
}
merged_handlers.sort_by_key(|h| h.start_offset);
AssembledBytecode {
bytecode,
source_map,
exception_handlers: merged_handlers,
basic_block_start_offsets,
number_of_registers,
number_of_arguments,
}
}
}
/// Result of assembling bytecode from basic blocks.
pub struct AssembledBytecode {
pub bytecode: Vec<u8>,
pub source_map: Vec<SourceMapEntry>,
pub exception_handlers: Vec<ExceptionHandler>,
pub basic_block_start_offsets: Vec<usize>,
pub number_of_registers: u32,
/// One past the highest `Operand::argument` index referenced by any
/// instruction, or 0 if the bytecode never reads an argument. Used by
/// the validator as the upper bound for argument operands.
pub number_of_arguments: u32,
}
/// Exception handler range (with byte offsets, post-linking).
#[derive(Debug, Clone)]
pub struct ExceptionHandler {
pub start_offset: u32,
pub end_offset: u32,
pub handler_offset: u32,
}
/// A typed constant value stored in the constant pool.
///
/// The actual NaN-boxed encoding happens at the FFI boundary when
/// creating the `Bytecode::Executable`.
#[derive(Debug, Clone)]
pub enum ConstantValue {
Number(f64),
Boolean(bool),
Null,
Undefined,
Empty,
String(Utf16String),
BigInt(String),
/// A VM-specific well-known symbol resolved when the Executable is materialized.
WellKnownSymbol(WellKnownSymbolKind),
/// A NativeJavaScriptBackedFunction intrinsic resolved when the Executable is materialized.
AbstractOperation(AbstractOperationKind),
}
/// Convert a constant value to a boolean, matching JS `ToBoolean`.
/// Returns `None` for VM-specific constants whose truthiness cannot be
/// determined until the Executable is materialized.
pub fn constant_to_boolean(value: &ConstantValue) -> Option<bool> {
match value {
ConstantValue::Boolean(b) => Some(*b),
ConstantValue::Null | ConstantValue::Undefined | ConstantValue::Empty => Some(false),
ConstantValue::Number(n) => Some(*n != 0.0 && !n.is_nan()),
ConstantValue::String(s) => Some(!s.is_empty()),
ConstantValue::BigInt(s) => parse_bigint(s).map(|bi| bi != num_bigint::BigInt::ZERO),
ConstantValue::WellKnownSymbol(_) | ConstantValue::AbstractOperation(_) => None,
}
}
/// Parse a BigInt string to an arbitrary-precision BigInt.
/// Handles decimal, 0b binary, 0o octal, and 0x hex prefixes.
pub fn parse_bigint(s: &str) -> Option<num_bigint::BigInt> {
use num_bigint::BigInt;
if s.len() > 2 {
let (prefix, rest) = s.split_at(2);
match prefix {
"0b" | "0B" => return BigInt::parse_bytes(rest.as_bytes(), 2),
"0o" | "0O" => return BigInt::parse_bytes(rest.as_bytes(), 8),
"0x" | "0X" => return BigInt::parse_bytes(rest.as_bytes(), 16),
_ => {}
}
}
s.parse::<BigInt>().ok()
}
/// Use `preferred_dst` if available, otherwise allocate a fresh register.
pub fn choose_dst(generator: &mut Generator, preferred_dst: Option<&ScopedOperand>) -> ScopedOperand {
match preferred_dst {
Some(dst) => dst.clone(),
None => generator.allocate_register(),
}
}