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.
2067 lines
82 KiB
Rust
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(®, 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(®, &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(),
|
|
}
|
|
}
|