diff --git a/benches/scripts/basic/this-arrow.js b/benches/scripts/basic/this-arrow.js new file mode 100644 index 00000000000..82a6806053c --- /dev/null +++ b/benches/scripts/basic/this-arrow.js @@ -0,0 +1,13 @@ +const obj = { + value: 0, + increment() { + const add = () => { + this.value += 1; + }; + add(); + }, +}; +const start = Date.now(); +for (let i = 0; i < 1000000; i++) { + obj.increment(); +} diff --git a/core/ast/src/scope.rs b/core/ast/src/scope.rs index c172877479f..75a927b68d8 100644 --- a/core/ast/src/scope.rs +++ b/core/ast/src/scope.rs @@ -716,6 +716,11 @@ pub struct FunctionScopes { pub(crate) lexical_scope: Option, pub(crate) mapped_arguments_object: bool, pub(crate) requires_function_scope: bool, + /// Set when the *only* reason the function would need a runtime + /// function-environment is that `this` escapes into an inner arrow. + /// When this is `true`, the byte-compiler avoids `HAS_FUNCTION_SCOPE` + /// and instead propagates `this` directly to inner arrow closures. + pub(crate) this_escaped_only: bool, } impl FunctionScopes { @@ -761,6 +766,13 @@ impl FunctionScopes { self.requires_function_scope } + /// Returns `true` when `escaped_this()` is the sole reason a function + /// scope would be needed (no `super`, `new.target`, or escaping bindings). + #[must_use] + pub fn this_escaped_only(&self) -> bool { + self.this_escaped_only + } + /// Returns the parameters eval scope for this function. #[must_use] pub fn parameters_eval_scope(&self) -> Option<&Scope> { @@ -840,6 +852,7 @@ impl<'a> arbitrary::Arbitrary<'a> for FunctionScopes { lexical_scope: None, mapped_arguments_object: false, requires_function_scope: false, + this_escaped_only: false, }) } } diff --git a/core/ast/src/scope_analyzer.rs b/core/ast/src/scope_analyzer.rs index 079ebc197bb..ba60852b582 100644 --- a/core/ast/src/scope_analyzer.rs +++ b/core/ast/src/scope_analyzer.rs @@ -1693,12 +1693,22 @@ impl ScopeIndexVisitor { self.index += 1; } else if !arrow { assert!(scopes.function_scope().is_function()); - scopes.requires_function_scope = scopes.function_scope().escaped_this() - || contains(parameters, ContainsSymbol::Super) - || contains(body, ContainsSymbol::Super) - || contains(parameters, ContainsSymbol::NewTarget) + let escaped_this = scopes.function_scope().escaped_this(); + let has_super = contains(parameters, ContainsSymbol::Super) + || contains(body, ContainsSymbol::Super); + let has_new_target = contains(parameters, ContainsSymbol::NewTarget) || contains(body, ContainsSymbol::NewTarget); - self.index += u32::from(scopes.requires_function_scope); + + if escaped_this && !has_super && !has_new_target { + // `this` escapes (inner arrow captures it), but nothing else + // requires a function-environment at runtime. + scopes.this_escaped_only = true; + // Do NOT set requires_function_scope or increment index — + // no runtime environment will be pushed. + } else { + scopes.requires_function_scope = escaped_this || has_super || has_new_target; + self.index += u32::from(scopes.requires_function_scope); + } } scopes.function_scope.set_index(self.index); @@ -1889,6 +1899,7 @@ fn function_declaration_instantiation( lexical_scope: None, mapped_arguments_object: false, requires_function_scope: false, + this_escaped_only: false, }; // 1. Let calleeContext be the running execution context. diff --git a/core/engine/src/builtins/eval/mod.rs b/core/engine/src/builtins/eval/mod.rs index 07f32265398..24f384b75ef 100644 --- a/core/engine/src/builtins/eval/mod.rs +++ b/core/engine/src/builtins/eval/mod.rs @@ -241,15 +241,19 @@ impl Eval { } }); - let (var_environment, mut variable_scope) = - if let Some(e) = context.vm.frame().environments.outer_function_environment() { - (e.0, e.1) - } else { - ( - context.realm().environment().clone(), - context.realm().scope().clone(), - ) - }; + let (var_environment, mut variable_scope) = if let Some(e) = context + .vm + .frame_mut() + .environments + .outer_function_environment() + { + (e.0, e.1) + } else { + ( + context.realm().environment().clone(), + context.realm().scope().clone(), + ) + }; let lexical_scope = lexical_scope.unwrap_or(context.realm().scope().clone()); let lexical_scope = Scope::new(lexical_scope, strict); @@ -329,6 +333,10 @@ impl Eval { } let env_fp = context.vm.frame().environments.len() as u32; + // Promote all inline environments before cloning so that the eval + // frame and the enclosing frame share the same Gc-managed environments. + // This is correct because eval can add bindings to existing scopes. + context.vm.frame_mut().environments.promote_all(); let environments = context.vm.frame().environments.clone(); let realm = context.realm().clone(); context.vm.push_frame_with_stack( diff --git a/core/engine/src/builtins/function/mod.rs b/core/engine/src/builtins/function/mod.rs index 794cf17d19f..ed3a3fb9bb8 100644 --- a/core/engine/src/builtins/function/mod.rs +++ b/core/engine/src/builtins/function/mod.rs @@ -180,6 +180,11 @@ pub struct OrdinaryFunction { /// The [`Realm`] the function is defined in. pub(crate) realm: Realm, + /// Captured `this` for arrow functions compiled under + /// the `THIS_ESCAPED_ONLY` optimisation. Set at closure-creation time + /// by `SetArrowLexicalThis`, read at call time. + pub(crate) lexical_this: Option, + /// The `[[Fields]]` internal slot. fields: ThinVec, @@ -221,6 +226,7 @@ impl OrdinaryFunction { home_object: None, script_or_module, realm, + lexical_this: None, fields: ThinVec::default(), private_methods: ThinVec::default(), } @@ -1007,6 +1013,7 @@ pub(crate) fn function_call( let code = function.code.clone(); let environments = function.environments.clone(); let script_or_module = function.script_or_module.clone(); + let captured_lexical_this = function.lexical_this.clone(); drop(function); @@ -1031,6 +1038,17 @@ pub(crate) fn function_call( let lexical_this_mode = context.vm.frame().code_block.this_mode == ThisMode::Lexical; let this = if lexical_this_mode { + // Arrow function. If the enclosing function used the + // THIS_ESCAPED_ONLY optimisation, the resolved `this` was stored + // directly on this arrow closure — use it instead of walking the + // environment chain. + if let Some(this) = captured_lexical_this { + context + .vm + .stack + .set_this(context.vm.frames.last().expect("frame must exist"), this); + context.vm.frame_mut().flags |= CallFrameFlags::THIS_VALUE_CACHED; + } ThisBindingStatus::Lexical } else { let this = context.vm.stack.get_this(context.vm.frame()); diff --git a/core/engine/src/builtins/generator/mod.rs b/core/engine/src/builtins/generator/mod.rs index bafecdf4d50..8e86b52d887 100644 --- a/core/engine/src/builtins/generator/mod.rs +++ b/core/engine/src/builtins/generator/mod.rs @@ -65,6 +65,9 @@ pub(crate) struct GeneratorContext { impl GeneratorContext { /// Creates a new `GeneratorContext` from the current `Context` state. pub(crate) fn from_current(context: &mut Context, async_generator: Option) -> Self { + // Promote all inline environments before cloning so that the generator + // frame and the current frame share the same Gc-managed environments. + context.vm.frame_mut().environments.promote_all(); let mut frame = context.vm.frame().clone(); frame.environments = context.vm.frame().environments.clone(); frame.realm = context.realm().clone(); diff --git a/core/engine/src/builtins/json/mod.rs b/core/engine/src/builtins/json/mod.rs index 4ffa9d32c03..a0f69ab6bf9 100644 --- a/core/engine/src/builtins/json/mod.rs +++ b/core/engine/src/builtins/json/mod.rs @@ -302,6 +302,9 @@ impl Json { let realm = context.realm().clone(); let env_fp = context.vm.frame().environments.len() as u32; + // Promote all inline environments before cloning so that the JSON + // frame and the enclosing frame share the same Gc-managed environments. + context.vm.frame_mut().environments.promote_all(); context.vm.push_frame_with_stack( CallFrame::new( code_block, diff --git a/core/engine/src/bytecompiler/function.rs b/core/engine/src/bytecompiler/function.rs index 9316e28ae6b..f5bcd91a37a 100644 --- a/core/engine/src/bytecompiler/function.rs +++ b/core/engine/src/bytecompiler/function.rs @@ -24,6 +24,10 @@ pub(crate) struct FunctionCompiler { method: bool, in_with: bool, force_function_scope: bool, + /// When `true`, inner arrow functions should have `this` propagated + /// via `SetArrowLexicalThis` instead of inheriting it from the + /// environment chain. + propagate_lexical_this: bool, name_scope: Option, spanned_source_text: SpannedSourceText, source_path: SourcePath, @@ -41,6 +45,7 @@ impl FunctionCompiler { method: false, in_with: false, force_function_scope: false, + propagate_lexical_this: false, name_scope: None, spanned_source_text, source_path: SourcePath::None, @@ -105,6 +110,12 @@ impl FunctionCompiler { self } + /// Propagate lexical `this` to inner arrow functions. + pub(crate) const fn propagate_lexical_this(mut self, v: bool) -> Self { + self.propagate_lexical_this = v; + self + } + /// Set source map file path. pub(crate) fn source_path(mut self, source_path: SourcePath) -> Self { self.source_path = source_path; @@ -149,6 +160,11 @@ impl FunctionCompiler { if self.arrow { compiler.this_mode = ThisMode::Lexical; + // Arrow functions continue propagating `this` to nested arrows + // when the enclosing non-arrow function uses THIS_ESCAPED_ONLY. + if self.propagate_lexical_this { + compiler.propagate_lexical_this = true; + } } if let Some(scope) = self.name_scope @@ -161,10 +177,18 @@ impl FunctionCompiler { if contains_direct_eval || !scopes.function_scope().all_bindings_local() { compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE; } else if !self.arrow { - compiler.code_block_flags.set( - CodeBlockFlags::HAS_FUNCTION_SCOPE, - self.force_function_scope || scopes.requires_function_scope(), - ); + if scopes.this_escaped_only() && !self.force_function_scope { + // `this` escapes into inner arrows, but nothing else requires a + // function-environment. Mark the code block so the byte compiler + // emits `SetArrowLexicalThis` instead of allocating an env. + compiler.code_block_flags |= CodeBlockFlags::THIS_ESCAPED_ONLY; + compiler.propagate_lexical_this = true; + } else { + compiler.code_block_flags.set( + CodeBlockFlags::HAS_FUNCTION_SCOPE, + self.force_function_scope || scopes.requires_function_scope(), + ); + } } if compiler.code_block_flags.has_function_scope() { diff --git a/core/engine/src/bytecompiler/mod.rs b/core/engine/src/bytecompiler/mod.rs index 56de4dde6dd..c7a9e1821d2 100644 --- a/core/engine/src/bytecompiler/mod.rs +++ b/core/engine/src/bytecompiler/mod.rs @@ -540,6 +540,8 @@ pub struct ByteCompiler<'ctx> { /// Whether the function is in a `with` statement. pub(crate) in_with: bool, + pub(crate) propagate_lexical_this: bool, + /// Used to determine if a we emitted a `CreateUnmappedArgumentsObject` opcode pub(crate) emitted_mapped_arguments_object_opcode: bool, @@ -668,6 +670,7 @@ impl<'ctx> ByteCompiler<'ctx> { #[cfg(feature = "annex-b")] annex_b_function_names: Vec::new(), in_with, + propagate_lexical_this: false, emitted_mapped_arguments_object_opcode: false, global_lexs: Vec::new(), @@ -2324,6 +2327,7 @@ impl<'ctx> ByteCompiler<'ctx> { .strict(self.strict()) .arrow(arrow) .in_with(self.in_with) + .propagate_lexical_this(self.propagate_lexical_this && arrow) .name_scope(name_scope.cloned()) .source_path(self.source_path.clone()) .compile( @@ -2348,8 +2352,21 @@ impl<'ctx> ByteCompiler<'ctx> { dst: &Register, ) { let name = function.name; + let is_arrow = function.kind.is_arrow(); let index = self.function(function); self.emit_get_function(dst, index); + + // When the enclosing function uses THIS_ESCAPED_ONLY, eagerly + // resolve `this` and store it on the arrow function object so the + // function environment can be elided. + if self.propagate_lexical_this && is_arrow { + let this = self.register_allocator.alloc(); + self.bytecode.emit_this(this.variable()); + self.bytecode + .emit_set_arrow_lexical_this(dst.variable(), this.variable()); + self.register_allocator.dealloc(this); + } + match node_kind { NodeKind::Declaration => { self.emit_binding( diff --git a/core/engine/src/environments/mod.rs b/core/engine/src/environments/mod.rs index 319d1da27bc..39451de5280 100644 --- a/core/engine/src/environments/mod.rs +++ b/core/engine/src/environments/mod.rs @@ -27,8 +27,8 @@ mod runtime; pub(crate) use runtime::{ - DeclarativeEnvironment, Environment, EnvironmentStack, FunctionSlots, PrivateEnvironment, - SavedEnvironments, ThisBindingStatus, + DeclarativeEnvironment, EnvironmentStack, FunctionSlots, PrivateEnvironment, SavedEnvironments, + ThisBindingStatus, }; #[cfg(test)] diff --git a/core/engine/src/environments/runtime/mod.rs b/core/engine/src/environments/runtime/mod.rs index 6657a5496fb..edd4fb37b4a 100644 --- a/core/engine/src/environments/runtime/mod.rs +++ b/core/engine/src/environments/runtime/mod.rs @@ -3,7 +3,8 @@ use crate::{ object::{JsObject, PrivateName}, }; use boa_ast::scope::{BindingLocator, BindingLocatorScope, Scope}; -use boa_gc::{Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, Trace, custom_trace}; +use std::cell::Cell; use thin_vec::ThinVec; mod declarative; @@ -18,47 +19,226 @@ pub(crate) use self::{ private::PrivateEnvironment, }; -/// A single node in the environment chain. +/// A single node in the captured environment chain. /// -/// Each node holds one [`Environment`] and a pointer to its parent. -/// The chain is immutable from the perspective of other holders — pushing -/// creates a new tip node, leaving existing nodes untouched. This makes -/// cloning the chain O(1) (a single `Gc` ref-count bump). +/// Used only for environments inherited from closures (the "captured" region). +/// Locally-pushed environments live in a flat `Vec` instead. #[derive(Clone, Debug, Trace, Finalize)] pub(crate) struct EnvironmentNode { env: Environment, parent: Option>, } +/// A locally-pushed environment that has not yet been promoted to the GC heap. +/// +/// Environments start as `Inline` when pushed during function execution. They are +/// promoted to `Promoted` (Gc-managed) only when a closure captures them via +/// [`EnvironmentStack::snapshot_for_closure`]. After promotion, both the outer scope +/// and the closure share the same `Gc`. +#[derive(Debug)] +pub(crate) enum LocalEnvironment { + /// Bindings stored inline — no `Gc` allocation. + Inline { + kind: DeclarativeEnvironmentKind, + poisoned: Cell, + with: bool, + }, + /// Promoted to GC heap after closure capture. + Promoted(Gc), + /// Object environment (for `with` statements). + Object(JsObject), + /// Temporary sentinel used during promotion. Never visible externally. + _Vacant, +} + +impl Finalize for LocalEnvironment {} + +// SAFETY: We trace all GC-managed fields in each variant. +unsafe impl Trace for LocalEnvironment { + custom_trace!(this, mark, { + match this { + Self::Inline { kind, .. } => mark(kind), + Self::Promoted(gc) => mark(gc), + Self::Object(obj) => mark(obj), + Self::_Vacant => {} + } + }); +} + +impl Clone for LocalEnvironment { + fn clone(&self) -> Self { + match self { + Self::Promoted(gc) => Self::Promoted(gc.clone()), + Self::Object(obj) => Self::Object(obj.clone()), + Self::Inline { .. } => { + panic!("Cannot clone inline local environment; promote first") + } + Self::_Vacant => panic!("cannot clone vacant environment"), + } + } +} + +impl LocalEnvironment { + /// Returns the `DeclarativeEnvironmentKind` if this is a declarative environment. + fn as_declarative_kind(&self) -> Option<&DeclarativeEnvironmentKind> { + match self { + Self::Inline { kind, .. } => Some(kind), + Self::Promoted(gc) => Some(gc.kind()), + Self::Object(_) | Self::_Vacant => None, + } + } + + /// Returns `true` if this is a declarative environment (not object). + fn is_declarative(&self) -> bool { + !matches!(self, Self::Object(_) | Self::_Vacant) + } + + /// Returns the `poisoned` flag. + fn poisoned(&self) -> bool { + match self { + Self::Inline { poisoned, .. } => poisoned.get(), + Self::Promoted(gc) => gc.poisoned(), + Self::Object(_) | Self::_Vacant => false, + } + } + + /// Returns the `with` flag. + fn with(&self) -> bool { + match self { + Self::Inline { with, .. } => *with, + Self::Promoted(gc) => gc.with(), + Self::Object(_) | Self::_Vacant => false, + } + } + + /// Sets the `poisoned` flag. + fn poison(&self) { + match self { + Self::Inline { poisoned, .. } => poisoned.set(true), + Self::Promoted(gc) => gc.poison(), + Self::Object(_) | Self::_Vacant => {} + } + } + + /// Returns `true` if this is a function environment. + fn is_function(&self) -> bool { + self.as_declarative_kind() + .is_some_and(|k| matches!(k, DeclarativeEnvironmentKind::Function(_))) + } + + /// Gets a binding value. + fn get_binding(&self, index: u32) -> Option { + match self { + Self::Inline { kind, .. } => kind.get(index), + Self::Promoted(gc) => gc.get(index), + Self::Object(_) | Self::_Vacant => panic!("not a declarative environment"), + } + } + + /// Sets a binding value. + fn set_binding(&self, index: u32, value: JsValue) { + match self { + Self::Inline { kind, .. } => kind.set(index, value), + Self::Promoted(gc) => gc.set(index, value), + Self::Object(_) | Self::_Vacant => panic!("not a declarative environment"), + } + } + + /// Promote this inline environment to a `Gc`. + /// + /// If already promoted, returns the existing `Gc`. Panics for object environments. + fn promote_to_gc(&mut self) -> Gc { + if let Self::Promoted(gc) = self { + return gc.clone(); + } + + let old = std::mem::replace(self, Self::_Vacant); + match old { + Self::Inline { + kind, + poisoned, + with, + } => { + let gc = Gc::new(DeclarativeEnvironment::new(kind, poisoned.get(), with)); + *self = Self::Promoted(gc.clone()); + gc + } + other => { + *self = other; + panic!("cannot promote non-declarative local environment"); + } + } + } +} + /// The environment stack holds all environments at runtime. /// -/// Implemented as a singly-linked list of [`EnvironmentNode`]s, where each -/// node points to its parent (the previous environment). Cloning is O(1) — -/// just a reference-count bump on the tip pointer plus small scalar copies. +/// Split into two regions for performance: +/// - **Captured**: A linked list of `Gc` inherited from the closure +/// chain. These are already heap-allocated. Accessed via linked-list traversal. +/// - **Local**: A flat `Vec` for environments pushed during the +/// current function's execution. No `Gc` allocation on push. Accessed via O(1) +/// Vec indexing. +/// +/// When a closure captures the environment +/// ([`EnvironmentStack::snapshot_for_closure`]), all local inline environments are +/// promoted to `Gc` and linked into the captured chain. +/// After promotion, both the outer scope and the closure share the same `Gc` pointers. /// /// The global declarative environment is NOT stored here — it lives in the /// [`crate::realm::Realm`] and is accessed via `frame.realm.environment()`. -/// This avoids a `Gc` clone on every function call. -#[derive(Clone, Debug, Trace, Finalize)] +#[derive(Debug)] pub(crate) struct EnvironmentStack { - /// The tip (most recently pushed) environment in the chain. - tip: Option>, + /// Environments inherited from the closure chain (already Gc-managed). + captured_tip: Option>, + + /// Number of environments in the captured chain. + #[allow(dead_code)] + captured_depth: u32, - /// Number of environments in the chain (not counting global). - #[unsafe_ignore_trace] - depth: u32, + /// Environments pushed during this function's execution (flat, no Gc on push). + local: Vec, private_stack: ThinVec>, } +impl Finalize for EnvironmentStack {} + +// SAFETY: We trace all GC-managed fields. +unsafe impl Trace for EnvironmentStack { + custom_trace!(this, mark, { + mark(&this.captured_tip); + for env in &this.local { + mark(env); + } + mark(&this.private_stack); + }); +} + +impl Clone for EnvironmentStack { + fn clone(&self) -> Self { + // Clone is used by: + // - OrdinaryFunction::environments.clone() in function_call (local is always empty) + // - Generator frame cloning (should promote_all first) + // For safety, all Inline entries must have been promoted before cloning. + Self { + captured_tip: self.captured_tip.clone(), + captured_depth: self.captured_depth, + local: self.local.clone(), + private_stack: self.private_stack.clone(), + } + } +} + /// Saved environment state for `pop_to_global` / `restore_from_saved`. /// Used by indirect `eval` and `Function.prototype.toString` recompilation. pub(crate) struct SavedEnvironments { - tip: Option>, - depth: u32, + captured_tip: Option>, + captured_depth: u32, + local: Vec, } -/// A runtime environment. +/// A runtime environment (used in the captured linked-list chain). #[derive(Clone, Debug, Trace, Finalize)] pub(crate) enum Environment { Declarative(Gc), @@ -79,90 +259,247 @@ impl EnvironmentStack { /// Create a new environment stack. pub(crate) fn new() -> Self { Self { - tip: None, - depth: 0, + captured_tip: None, + captured_depth: 0, + local: Vec::new(), private_stack: ThinVec::new(), } } - /// Gets the next outer function environment. - pub(crate) fn outer_function_environment(&self) -> Option<(Gc, Scope)> { - for (env, _) in self.iter_from_tip() { - if let Some(decl) = env.as_declarative() - && let Some(function_env) = decl.kind().as_function() - { - return Some((decl.clone(), function_env.compile().clone())); + /// Get the total number of environments (captured + local), not counting global. + #[inline] + pub(crate) fn len(&self) -> usize { + self.captured_depth as usize + self.local.len() + } + + // ---- Push operations (allocation-free for Inline) ---- + + /// Push a lexical environment and return its absolute index. + pub(crate) fn push_lexical( + &mut self, + bindings_count: u32, + global: &Gc, + ) -> u32 { + let (poisoned, with) = self.compute_poisoned_with(global); + let index = self.len() as u32; + self.local.push(LocalEnvironment::Inline { + kind: DeclarativeEnvironmentKind::Lexical(LexicalEnvironment::new(bindings_count)), + poisoned: Cell::new(poisoned), + with, + }); + index + } + + /// Push a function environment on the environments stack. + pub(crate) fn push_function( + &mut self, + scope: Scope, + function_slots: FunctionSlots, + global: &Gc, + ) { + let num_bindings = scope.num_bindings_non_local(); + let (poisoned, with) = self.compute_poisoned_with(global); + self.local.push(LocalEnvironment::Inline { + kind: DeclarativeEnvironmentKind::Function(FunctionEnvironment::new( + num_bindings, + function_slots, + scope, + )), + poisoned: Cell::new(poisoned), + with, + }); + } + + /// Push an object environment (for `with` statements). + pub(crate) fn push_object(&mut self, object: JsObject) { + self.local.push(LocalEnvironment::Object(object)); + } + + /// Push a module environment on the environments stack. + /// + /// Module environments are immediately promoted to Gc because they are + /// global singletons that need to be shared across module boundaries. + pub(crate) fn push_module(&mut self, scope: Scope) { + let num_bindings = scope.num_bindings_non_local(); + let gc = Gc::new(DeclarativeEnvironment::new( + DeclarativeEnvironmentKind::Module(ModuleEnvironment::new(num_bindings, scope)), + false, + false, + )); + self.local.push(LocalEnvironment::Promoted(gc)); + } + + // ---- Pop / Truncate ---- + + /// Pop the most recently pushed environment. + #[track_caller] + pub(crate) fn pop(&mut self) { + if self.local.pop().is_some() { + return; + } + // Fall back to popping from captured chain (shouldn't normally happen + // within a single frame's execution). + let node = self + .captured_tip + .as_ref() + .expect("cannot pop empty environment chain"); + self.captured_tip = node.parent.clone(); + self.captured_depth -= 1; + } + + /// Truncate current environments to the given total depth. + pub(crate) fn truncate(&mut self, len: usize) { + let captured = self.captured_depth as usize; + if len >= captured { + // Only truncate local environments. + self.local.truncate(len - captured); + } else { + // Truncate all locals and some captured. + self.local.clear(); + while (self.captured_depth as usize) > len { + let node = self + .captured_tip + .as_ref() + .expect("depth > 0 implies tip is Some"); + self.captured_tip = node.parent.clone(); + self.captured_depth -= 1; } } - None } /// Save all current environments and clear the stack. /// Used by indirect eval and `Function.prototype.toString` recompilation. pub(crate) fn pop_to_global(&mut self) -> SavedEnvironments { SavedEnvironments { - tip: self.tip.take(), - depth: std::mem::replace(&mut self.depth, 0), + captured_tip: self.captured_tip.take(), + captured_depth: std::mem::replace(&mut self.captured_depth, 0), + local: std::mem::take(&mut self.local), } } /// Restore environments from a previous `pop_to_global` call. pub(crate) fn restore_from_saved(&mut self, saved: SavedEnvironments) { - self.tip = saved.tip; - self.depth = saved.depth; + self.captured_tip = saved.captured_tip; + self.captured_depth = saved.captured_depth; + self.local = saved.local; } - /// Get the number of current environments (not counting global). - #[inline] - pub(crate) fn len(&self) -> usize { - self.depth as usize + // ---- Access methods ---- + + /// Get a binding value from the environment at `env_index`. + #[track_caller] + pub(crate) fn get_binding_value(&self, env_index: u32, binding_index: u32) -> Option { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + // Local environment — O(1) access. + self.local[idx - captured].get_binding(binding_index) + } else { + // Captured environment — linked-list traversal. + let env = self.get_captured(idx).expect("index in range"); + env.as_declarative() + .expect("must be declarative") + .get(binding_index) + } } - /// Get the environment at the given absolute index (0-based from root). - /// - /// Index 0 is the deepest (first pushed) environment in the chain. - /// Index `len() - 1` is the tip (most recently pushed). - #[inline] - pub(crate) fn get(&self, index: usize) -> Option<&Environment> { - let depth = self.depth as usize; - if index >= depth { - return None; + /// Set a binding value in the environment at `env_index`. + #[track_caller] + pub(crate) fn set_binding_value(&self, env_index: u32, binding_index: u32, value: JsValue) { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + self.local[idx - captured].set_binding(binding_index, value); + } else { + let env = self.get_captured(idx).expect("index in range"); + env.as_declarative() + .expect("must be declarative") + .set(binding_index, value); } - let steps = depth - 1 - index; - let mut current = self.tip.as_deref()?; - for _ in 0..steps { - current = current.parent.as_deref()?; + } + + /// Check if the environment at `env_index` is an object environment. + pub(crate) fn is_object_env(&self, env_index: u32) -> bool { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + matches!(self.local[idx - captured], LocalEnvironment::Object(_)) + } else { + matches!(self.get_captured(idx), Some(Environment::Object(_))) } - Some(¤t.env) } - /// Iterate from tip (most recent) toward root (oldest). - /// Yields `(&Environment, absolute_index)`. - fn iter_from_tip(&self) -> EnvironmentChainIter<'_> { - EnvironmentChainIter { - current: self.tip.as_deref(), - index: self.depth, + /// Get the object from an object environment at `env_index`. + pub(crate) fn get_object_env(&self, env_index: u32) -> Option<&JsObject> { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + match &self.local[idx - captured] { + LocalEnvironment::Object(obj) => Some(obj), + _ => None, + } + } else { + match self.get_captured(idx)? { + Environment::Object(obj) => Some(obj), + Environment::Declarative(_) => None, + } } } - /// Get the tip (most recently pushed) environment. - #[inline] - fn last(&self) -> Option<&Environment> { - self.tip.as_deref().map(|node| &node.env) + /// Get the `DeclarativeEnvironmentKind` at the given absolute index. + #[allow(dead_code)] + pub(crate) fn get_declarative_kind( + &self, + env_index: u32, + ) -> Option<&DeclarativeEnvironmentKind> { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + self.local[idx - captured].as_declarative_kind() + } else { + self.get_captured(idx)?.as_declarative().map(|gc| gc.kind()) + } } - /// Truncate current environments to the given depth. - pub(crate) fn truncate(&mut self, len: usize) { - while self.depth as usize > len { - let node = self.tip.as_ref().expect("depth > 0 implies tip is Some"); - self.tip = node.parent.clone(); - self.depth -= 1; + /// Get the `Gc` at the given index, promoting if needed. + #[allow(dead_code)] + pub(crate) fn get_declarative_gc( + &mut self, + env_index: u32, + ) -> Option> { + let captured = self.captured_depth as usize; + let idx = env_index as usize; + if idx >= captured { + let local = &mut self.local[idx - captured]; + if local.is_declarative() { + Some(local.promote_to_gc()) + } else { + None + } + } else { + self.get_captured(idx)?.as_declarative().cloned() } } + /// Get the environment at the given absolute index in the captured chain. + fn get_captured(&self, index: usize) -> Option<&Environment> { + let depth = self.captured_depth as usize; + if index >= depth { + return None; + } + let steps = depth - 1 - index; + let mut current = self.captured_tip.as_deref()?; + for _ in 0..steps { + current = current.parent.as_deref()?; + } + Some(¤t.env) + } + + // ---- This environment ---- + /// `GetThisEnvironment` /// - /// Returns the environment that currently provides a `this` biding. + /// Returns the environment that currently provides a `this` binding. /// /// More information: /// - [ECMAScript specification][spec] @@ -172,135 +509,178 @@ impl EnvironmentStack { &'a self, global: &'a Gc, ) -> &'a DeclarativeEnvironmentKind { - for (env, _) in self.iter_from_tip() { + // Search local environments first (tip to base). + for local in self.local.iter().rev() { + if let Some(kind) = local.as_declarative_kind() + && kind.has_this_binding() + { + return kind; + } + } + // Then search captured chain. + for (env, _) in self.iter_captured() { if let Some(decl) = env.as_declarative().filter(|decl| decl.has_this_binding()) { return decl.kind(); } } - global.kind() } /// `GetThisBinding` /// /// Returns the current `this` binding of the environment. - /// Note: If the current environment is the global environment, this function returns `Ok(None)`. - /// - /// More information: - /// - [ECMAScript specification][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-function-environment-records-getthisbinding pub(crate) fn get_this_binding(&self) -> JsResult> { - for (env, _) in self.iter_from_tip() { + // Search local environments first. + for local in self.local.iter().rev() { + match local { + LocalEnvironment::Inline { kind, .. } => { + if let Some(this) = kind.get_this_binding()? { + return Ok(Some(this)); + } + } + LocalEnvironment::Promoted(gc) => { + if let Some(this) = gc.get_this_binding()? { + return Ok(Some(this)); + } + } + LocalEnvironment::Object(_) | LocalEnvironment::_Vacant => {} + } + } + // Then captured chain. + for (env, _) in self.iter_captured() { if let Environment::Declarative(decl) = env && let Some(this) = decl.get_this_binding()? { return Ok(Some(this)); } } - Ok(None) } - /// Push a new object environment on the environments stack. - pub(crate) fn push_object(&mut self, object: JsObject) { - self.push_env(Environment::Object(object)); - } + // ---- Outer function environment (for eval) ---- - /// Push a lexical environment on the environments stack and return it's index. - pub(crate) fn push_lexical( + /// Gets the next outer function environment. + pub(crate) fn outer_function_environment( &mut self, - bindings_count: u32, - global: &Gc, - ) -> u32 { - let (poisoned, with) = self.compute_poisoned_with(global); - - let index = self.depth; - - self.push_env(Environment::Declarative(Gc::new( - DeclarativeEnvironment::new( - DeclarativeEnvironmentKind::Lexical(LexicalEnvironment::new(bindings_count)), - poisoned, - with, - ), - ))); - - index + ) -> Option<(Gc, Scope)> { + // Search local environments first. Force-promote if found. + for (i, local) in self.local.iter().enumerate().rev() { + if local.is_function() { + let gc = self.local[i].promote_to_gc(); + let scope = gc + .kind() + .as_function() + .expect("must be function") + .compile() + .clone(); + return Some((gc, scope)); + } + } + // Then captured chain. + let mut current = self.captured_tip.as_deref(); + while let Some(node) = current { + if let Some(decl) = node.env.as_declarative() + && let Some(function_env) = decl.kind().as_function() + { + return Some((decl.clone(), function_env.compile().clone())); + } + current = node.parent.as_deref(); + } + None } - /// Push a function environment on the environments stack. - pub(crate) fn push_function( - &mut self, - scope: Scope, - function_slots: FunctionSlots, - global: &Gc, - ) { - let num_bindings = scope.num_bindings_non_local(); - - let (poisoned, with) = self.compute_poisoned_with(global); + // ---- Current tip checks ---- - self.push_env(Environment::Declarative(Gc::new( - DeclarativeEnvironment::new( - DeclarativeEnvironmentKind::Function(FunctionEnvironment::new( - num_bindings, - function_slots, - scope, - )), - poisoned, - with, - ), - ))); + /// Check if the tip (most recently pushed) environment is a declarative + /// environment that is not poisoned and not inside a `with`. + /// + /// Used as a fast-path check in `find_runtime_binding` and similar. + pub(crate) fn current_is_clean_declarative(&self, global: &Gc) -> bool { + if let Some(local) = self.local.last() { + local.is_declarative() && !local.poisoned() && !local.with() + } else if let Some(node) = self.captured_tip.as_deref() { + node.env + .as_declarative() + .is_some_and(|d| !d.poisoned() && !d.with()) + } else { + // Stack is empty, check global. + !global.poisoned() && !global.with() + } } - /// Push a module environment on the environments stack. - pub(crate) fn push_module(&mut self, scope: Scope) { - let num_bindings = scope.num_bindings_non_local(); - self.push_env(Environment::Declarative(Gc::new( - DeclarativeEnvironment::new( - DeclarativeEnvironmentKind::Module(ModuleEnvironment::new(num_bindings, scope)), - false, - false, - ), - ))); + /// Check if the tip is a declarative environment that is not inside a `with`. + pub(crate) fn current_is_not_with(&self, global: &Gc) -> bool { + if let Some(local) = self.local.last() { + local.is_declarative() && !local.with() + } else if let Some(node) = self.captured_tip.as_deref() { + node.env.as_declarative().is_some_and(|d| !d.with()) + } else { + !global.with() + } } - /// Pop environment from the environments stack. - #[track_caller] - pub(crate) fn pop(&mut self) { - let node = self - .tip - .as_ref() - .expect("cannot pop empty environment chain"); - self.tip = node.parent.clone(); - self.depth -= 1; + /// Get the `Gc` of the most recently pushed declarative + /// environment. Promotes if needed. + pub(crate) fn current_declarative_gc( + &mut self, + global: &Gc, + ) -> Option> { + if let Some(local) = self.local.last_mut() { + if local.is_declarative() { + return Some(local.promote_to_gc()); + } + return None; + } + if let Some(node) = self.captured_tip.as_deref() { + return node.env.as_declarative().cloned(); + } + Some(global.clone()) } - /// Get the most outer environment. - pub(crate) fn current_declarative_ref<'a>( + /// Get the `DeclarativeEnvironmentKind` of the most recently pushed environment. + pub(crate) fn current_declarative_kind<'a>( &'a self, global: &'a Gc, - ) -> Option<&'a Gc> { - if let Some(env) = self.last() { - env.as_declarative() - } else { - Some(global) + ) -> Option<&'a DeclarativeEnvironmentKind> { + if let Some(local) = self.local.last() { + return local.as_declarative_kind(); + } + if let Some(node) = self.captured_tip.as_deref() { + return node.env.as_declarative().map(|gc| gc.kind()); } + Some(global.kind()) } + // ---- Poison ---- + /// Mark that there may be added bindings from the current environment to the next function /// environment. pub(crate) fn poison_until_last_function(&mut self, global: &Gc) { - for (env, _) in self.iter_from_tip() { - if let Some(decl) = env.as_declarative() { + // Poison local environments from tip toward base. + for local in self.local.iter().rev() { + if local.is_declarative() { + local.poison(); + if local.is_function() { + return; + } + } + } + // Then captured chain. + let mut current = self.captured_tip.as_deref(); + while let Some(node) = current { + if let Some(decl) = node.env.as_declarative() { decl.poison(); if decl.is_function() { return; } } + current = node.parent.as_deref(); } global.poison(); } + // ---- Binding value helpers ---- + /// Set the value of a lexical binding. /// /// # Panics @@ -314,14 +694,14 @@ impl EnvironmentStack { value: JsValue, global: &Gc, ) { - let env = match environment { - BindingLocatorScope::GlobalObject | BindingLocatorScope::GlobalDeclarative => global, - BindingLocatorScope::Stack(index) => self - .get(index as usize) - .and_then(Environment::as_declarative) - .expect("must be declarative environment"), - }; - env.set(binding_index, value); + match environment { + BindingLocatorScope::GlobalObject | BindingLocatorScope::GlobalDeclarative => { + global.set(binding_index, value); + } + BindingLocatorScope::Stack(index) => { + self.set_binding_value(index, binding_index, value); + } + } } /// Set the value of a binding if it is uninitialized. @@ -337,18 +717,103 @@ impl EnvironmentStack { value: JsValue, global: &Gc, ) { - let env = match environment { - BindingLocatorScope::GlobalObject | BindingLocatorScope::GlobalDeclarative => global, - BindingLocatorScope::Stack(index) => self - .get(index as usize) - .and_then(Environment::as_declarative) - .expect("must be declarative environment"), - }; - if env.get(binding_index).is_none() { - env.set(binding_index, value); + match environment { + BindingLocatorScope::GlobalObject | BindingLocatorScope::GlobalDeclarative => { + if global.get(binding_index).is_none() { + global.set(binding_index, value); + } + } + BindingLocatorScope::Stack(index) => { + if self.get_binding_value(index, binding_index).is_none() { + self.set_binding_value(index, binding_index, value); + } + } + } + } + + // ---- Object environment checks ---- + + /// Indicate if the current environment stack has an object environment. + pub(crate) fn has_object_environment(&self) -> bool { + for local in self.local.iter().rev() { + if matches!(local, LocalEnvironment::Object(_)) { + return true; + } + } + for (env, _) in self.iter_captured() { + if matches!(env, Environment::Object(_)) { + return true; + } + } + false + } + + // ---- Closure capture ---- + + /// Create an `EnvironmentStack` snapshot suitable for storing in a closure. + /// + /// Promotes all inline local environments to `Gc` and + /// builds a linked-list chain. Both the outer scope and the closure share the + /// same `Gc` pointers after promotion. + pub(crate) fn snapshot_for_closure(&mut self) -> EnvironmentStack { + // Build a linked list from captured_tip + all locals. + let mut tip = self.captured_tip.clone(); + let mut depth = self.captured_depth; + + for local in &mut self.local { + match local { + LocalEnvironment::Inline { + kind: _, + poisoned: _, + with: _, + } => { + let gc = local.promote_to_gc(); + tip = Some(Gc::new(EnvironmentNode { + env: Environment::Declarative(gc), + parent: tip, + })); + depth += 1; + } + LocalEnvironment::Promoted(gc) => { + tip = Some(Gc::new(EnvironmentNode { + env: Environment::Declarative(gc.clone()), + parent: tip, + })); + depth += 1; + } + LocalEnvironment::Object(obj) => { + tip = Some(Gc::new(EnvironmentNode { + env: Environment::Object(obj.clone()), + parent: tip, + })); + depth += 1; + } + LocalEnvironment::_Vacant => panic!("vacant environment in stack"), + } + } + + EnvironmentStack { + captured_tip: tip, + captured_depth: depth, + local: Vec::new(), + private_stack: self.private_stack.clone(), + } + } + + /// Promote all inline local environments to Gc. + /// + /// Call this before cloning the `EnvironmentStack` (e.g., for generators). + #[allow(dead_code)] + pub(crate) fn promote_all(&mut self) { + for local in &mut self.local { + if matches!(local, LocalEnvironment::Inline { .. }) { + local.promote_to_gc(); + } } } + // ---- Private environments ---- + /// Push a private environment to the private environment stack. pub(crate) fn push_private(&mut self, environment: Gc) { self.private_stack.push(environment); @@ -387,55 +852,50 @@ impl EnvironmentStack { names } - /// Indicate if the current environment stack has an object environment. - pub(crate) fn has_object_environment(&self) -> bool { - self.iter_from_tip() - .any(|(env, _)| matches!(env, Environment::Object(_))) - } - - /// Create an `EnvironmentStack` snapshot suitable for storing in a closure. - /// - /// With the linked-list implementation, this is just a clone since - /// cloning is O(1) — a single ref-count bump on the tip pointer. - pub(crate) fn snapshot_for_closure(&self) -> EnvironmentStack { - self.clone() - } - // ---- Private helpers ---- - /// Push an environment onto the chain. - fn push_env(&mut self, env: Environment) { - self.tip = Some(Gc::new(EnvironmentNode { - env, - parent: self.tip.take(), - })); - self.depth += 1; + /// Iterate captured chain from tip toward root. + fn iter_captured(&self) -> CapturedChainIter<'_> { + CapturedChainIter { + current: self.captured_tip.as_deref(), + index: self.captured_depth, + } } /// Compute the `(poisoned, with)` flags for a new environment. fn compute_poisoned_with(&self, global: &Gc) -> (bool, bool) { - let with = if let Some(env) = self.last() { - env.as_declarative().is_none() + // Check if the tip is an object environment (for `with`). + let with = if let Some(local) = self.local.last() { + matches!(local, LocalEnvironment::Object(_)) + } else if let Some(node) = self.captured_tip.as_deref() { + node.env.as_declarative().is_none() } else { false }; - let environment = self - .iter_from_tip() - .find_map(|(env, _)| env.as_declarative()) - .unwrap_or(global); - (environment.poisoned(), with || environment.with()) + // Find the nearest declarative environment to check poisoned/with. + // Search local first, then captured. + for local in self.local.iter().rev() { + if local.is_declarative() { + return (local.poisoned(), with || local.with()); + } + } + for (env, _) in self.iter_captured() { + if let Some(decl) = env.as_declarative() { + return (decl.poisoned(), with || decl.with()); + } + } + (global.poisoned(), with || global.with()) } } -/// Iterator from tip (most recent) toward root (oldest). -/// Yields `(&Environment, absolute_index)`. -struct EnvironmentChainIter<'a> { +/// Iterator over the captured linked-list chain from tip toward root. +struct CapturedChainIter<'a> { current: Option<&'a EnvironmentNode>, index: u32, } -impl<'a> Iterator for EnvironmentChainIter<'a> { +impl<'a> Iterator for CapturedChainIter<'a> { type Item = (&'a Environment, u32); fn next(&mut self) -> Option { @@ -461,51 +921,90 @@ impl Context { /// semantics cannot add or remove lexical bindings. pub(crate) fn find_runtime_binding(&mut self, locator: &mut BindingLocator) -> JsResult<()> { let global = self.vm.frame().realm.environment(); - if let Some(env) = self.vm.frame().environments.current_declarative_ref(global) - && !env.with() - && !env.poisoned() + if self + .vm + .frame() + .environments + .current_is_clean_declarative(global) { return Ok(()); } - let (global, min_index) = match locator.scope() { + let (global_scope, min_index) = match locator.scope() { BindingLocatorScope::GlobalObject | BindingLocatorScope::GlobalDeclarative => (true, 0), BindingLocatorScope::Stack(index) => (false, index), }; let max_index = self.vm.frame().environments.len() as u32; for index in (min_index..max_index).rev() { - match self.environment_expect(index) { - Environment::Declarative(env) => { - if env.poisoned() { - if let Some(env) = env.kind().as_function() - && let Some(b) = env.compile().get_binding(locator.name()) - { - locator.set_scope(b.scope()); - locator.set_binding_index(b.binding_index()); - return Ok(()); - } - } else if !env.with() { - return Ok(()); + if self.vm.frame().environments.is_object_env(index) { + let obj = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); + let key = locator.name().clone(); + if obj.has_property(key.clone(), self)? { + if let Some(unscopables) = obj.get(JsSymbol::unscopables(), self)?.as_object() + && unscopables.get(key.clone(), self)?.to_boolean() + { + continue; } + locator.set_scope(BindingLocatorScope::Stack(index)); + return Ok(()); } - Environment::Object(o) => { - let o = o.clone(); - let key = locator.name().clone(); - if o.has_property(key.clone(), self)? { - if let Some(unscopables) = o.get(JsSymbol::unscopables(), self)?.as_object() - && unscopables.get(key.clone(), self)?.to_boolean() - { + } else { + // Declarative environment. + let poisoned = { + let captured = self.vm.frame().environments.captured_depth as usize; + let idx = index as usize; + if idx >= captured { + let local = &self.vm.frame().environments.local[idx - captured]; + ( + local.poisoned(), + local.with(), + local.is_function(), + local.as_declarative_kind(), + ) + } else { + let env = self + .vm + .frame() + .environments + .get_captured(idx) + .and_then(Environment::as_declarative); + if let Some(env) = env { + ( + env.poisoned(), + env.with(), + env.is_function(), + Some(env.kind()), + ) + } else { continue; } - locator.set_scope(BindingLocatorScope::Stack(index)); + } + }; + let (is_poisoned, is_with, is_function, kind) = poisoned; + if is_poisoned { + if let Some(kind) = kind + && let Some(func_env) = kind.as_function() + && let Some(b) = func_env.compile().get_binding(locator.name()) + { + locator.set_scope(b.scope()); + locator.set_binding_index(b.binding_index()); return Ok(()); } + } else if !is_with { + return Ok(()); } + let _ = is_function; } } - if global + if global_scope && self.realm().environment().poisoned() && let Some(b) = self.realm().scope().get_binding(locator.name()) { @@ -522,9 +1021,7 @@ impl Context { locator: &BindingLocator, ) -> JsResult> { let global = self.vm.frame().realm.environment(); - if let Some(env) = self.vm.frame().environments.current_declarative_ref(global) - && !env.with() - { + if self.vm.frame().environments.current_is_not_with(global) { return Ok(None); } @@ -535,28 +1032,56 @@ impl Context { let max_index = self.vm.frame().environments.len() as u32; for index in (min_index..max_index).rev() { - match self.environment_expect(index) { - Environment::Declarative(env) => { - if env.poisoned() { - if let Some(env) = env.kind().as_function() - && env.compile().get_binding(locator.name()).is_some() + if self.vm.frame().environments.is_object_env(index) { + let o = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); + let key = locator.name().clone(); + if o.has_property(key.clone(), self)? { + if let Some(unscopables) = o.get(JsSymbol::unscopables(), self)?.as_object() + && unscopables.get(key.clone(), self)?.to_boolean() + { + continue; + } + return Ok(Some(o)); + } + } else { + // Declarative environment. + let captured = self.vm.frame().environments.captured_depth as usize; + let idx = index as usize; + if idx >= captured { + let local = &self.vm.frame().environments.local[idx - captured]; + if local.poisoned() { + if let Some(kind) = local.as_declarative_kind() + && let Some(func_env) = kind.as_function() + && func_env.compile().get_binding(locator.name()).is_some() { break; } - } else if !env.with() { + } else if !local.with() { break; } - } - Environment::Object(o) => { - let o = o.clone(); - let key = locator.name().clone(); - if o.has_property(key.clone(), self)? { - if let Some(unscopables) = o.get(JsSymbol::unscopables(), self)?.as_object() - && unscopables.get(key.clone(), self)?.to_boolean() - { - continue; + } else { + let env = self + .vm + .frame() + .environments + .get_captured(idx) + .and_then(Environment::as_declarative); + if let Some(env) = env { + if env.poisoned() { + if let Some(func_env) = env.kind().as_function() + && func_env.compile().get_binding(locator.name()).is_some() + { + break; + } + } else if !env.with() { + break; } - return Ok(Some(o)); } } } @@ -581,14 +1106,26 @@ impl Context { let env = self.vm.frame().realm.environment(); Ok(env.get(locator.binding_index()).is_some()) } - BindingLocatorScope::Stack(index) => match self.environment_expect(index) { - Environment::Declarative(env) => Ok(env.get(locator.binding_index()).is_some()), - Environment::Object(obj) => { + BindingLocatorScope::Stack(index) => { + if self.vm.frame().environments.is_object_env(index) { + let obj = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); let key = locator.name().clone(); - let obj = obj.clone(); obj.has_property(key, self) + } else { + Ok(self + .vm + .frame() + .environments + .get_binding_value(index, locator.binding_index()) + .is_some()) } - }, + } } } @@ -609,14 +1146,25 @@ impl Context { let env = self.vm.frame().realm.environment(); Ok(env.get(locator.binding_index())) } - BindingLocatorScope::Stack(index) => match self.environment_expect(index) { - Environment::Declarative(env) => Ok(env.get(locator.binding_index())), - Environment::Object(obj) => { + BindingLocatorScope::Stack(index) => { + if self.vm.frame().environments.is_object_env(index) { + let obj = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); let key = locator.name().clone(); - let obj = obj.clone(); obj.get(key, self).map(Some) + } else { + Ok(self + .vm + .frame() + .environments + .get_binding_value(index, locator.binding_index())) } - }, + } } } @@ -642,16 +1190,25 @@ impl Context { let env = self.vm.frame().realm.environment(); env.set(locator.binding_index(), value); } - BindingLocatorScope::Stack(index) => match self.environment_expect(index) { - Environment::Declarative(decl) => { - decl.set(locator.binding_index(), value); - } - Environment::Object(obj) => { + BindingLocatorScope::Stack(index) => { + if self.vm.frame().environments.is_object_env(index) { + let obj = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); let key = locator.name().clone(); - let obj = obj.clone(); obj.set(key, value, strict, self)?; + } else { + self.vm.frame().environments.set_binding_value( + index, + locator.binding_index(), + value, + ); } - }, + } } Ok(()) } @@ -671,27 +1228,21 @@ impl Context { obj.__delete__(&key.into(), &mut self.into()) } BindingLocatorScope::GlobalDeclarative => Ok(false), - BindingLocatorScope::Stack(index) => match self.environment_expect(index) { - Environment::Declarative(_) => Ok(false), - Environment::Object(obj) => { + BindingLocatorScope::Stack(index) => { + if self.vm.frame().environments.is_object_env(index) { + let obj = self + .vm + .frame() + .environments + .get_object_env(index) + .expect("must be object env") + .clone(); let key = locator.name().clone(); - let obj = obj.clone(); obj.__delete__(&key.into(), &mut self.into()) + } else { + Ok(false) } - }, + } } } - - /// Return the environment at the given index. - /// - /// # Panics - /// - /// Panics if the `index` is out of range. - pub(crate) fn environment_expect(&self, index: u32) -> &Environment { - self.vm - .frame() - .environments - .get(index as usize) - .expect("environment index must be in range") - } } diff --git a/core/engine/src/module/source.rs b/core/engine/src/module/source.rs index 7583228c5a9..6916be610d9 100644 --- a/core/engine/src/module/source.rs +++ b/core/engine/src/module/source.rs @@ -1804,9 +1804,8 @@ impl SourceTextModule { let frame = context.vm.frame(); frame .environments - .current_declarative_ref(frame.realm.environment()) + .current_declarative_kind(frame.realm.environment()) .expect("must be declarative") - .kind() .as_module() .expect("last environment should be the module env") .set_indirect( @@ -1849,18 +1848,22 @@ impl SourceTextModule { } // 25. Remove moduleContext from the execution context stack. - let frame = context + let mut frame = context .vm .pop_frame() .expect("There should be a call frame"); - let env = frame - .environments - .current_declarative_ref(frame.realm.environment()) - .cloned() - .expect("frame must have a declarative environment"); + let env = { + let global = frame.realm.environment().clone(); + frame + .environments + .current_declarative_gc(&global) + .expect("frame must have a declarative environment") + }; // 16. Set module.[[Context]] to moduleContext. + // Promote all inline environments before cloning for the module context. + frame.environments.promote_all(); self.status.borrow_mut().transition(|state| match state { ModuleStatus::Linking { info } => ModuleStatus::PreLinked { environment: env, diff --git a/core/engine/src/module/synthetic.rs b/core/engine/src/module/synthetic.rs index e68e86322ff..cb123d8b775 100644 --- a/core/engine/src/module/synthetic.rs +++ b/core/engine/src/module/synthetic.rs @@ -351,8 +351,7 @@ impl SyntheticModule { } let env = envs - .current_declarative_ref(&global_env) - .cloned() + .current_declarative_gc(&global_env) .expect("should have the module environment"); self.state diff --git a/core/engine/src/vm/code_block.rs b/core/engine/src/vm/code_block.rs index 13e8b0abced..8656e5edbc1 100644 --- a/core/engine/src/vm/code_block.rs +++ b/core/engine/src/vm/code_block.rs @@ -51,6 +51,8 @@ bitflags! { /// If the function requires a function scope. const HAS_FUNCTION_SCOPE = 0b1_0000_0000; + const THIS_ESCAPED_ONLY = 0b10_0000_0000; + /// Trace instruction execution to `stdout`. #[cfg(feature = "trace")] const TRACEABLE = 0b1000_0000_0000_0000; @@ -848,6 +850,12 @@ impl CodeBlock { Instruction::GetFunctionObject { function_object } => { format!("function_object:{function_object}") } + Instruction::SetArrowLexicalThis { + function, + this_value, + } => { + format!("function:{function}, this_value:{this_value}") + } Instruction::Pop | Instruction::DeleteSuperThrow | Instruction::ReThrow @@ -865,8 +873,7 @@ impl CodeBlock { | Instruction::PopPrivateEnvironment | Instruction::Generator | Instruction::AsyncGenerator => String::new(), - Instruction::Reserved1 - | Instruction::Reserved2 + Instruction::Reserved2 | Instruction::Reserved3 | Instruction::Reserved4 | Instruction::Reserved5 @@ -1094,9 +1101,10 @@ pub(crate) fn create_function_object( let is_async = code.is_async(); let is_generator = code.is_generator(); + let env_snapshot = context.vm.frame_mut().environments.snapshot_for_closure(); let function = OrdinaryFunction::new( code, - context.vm.frame().environments.snapshot_for_closure(), + env_snapshot, script_or_module, context.realm().clone(), ); @@ -1163,9 +1171,10 @@ pub(crate) fn create_function_object_fast(code: Gc, context: &mut Con let is_async = code.is_async(); let is_generator = code.is_generator(); let has_prototype_property = code.has_prototype_property(); + let env_snapshot = context.vm.frame_mut().environments.snapshot_for_closure(); let function = OrdinaryFunction::new( code, - context.vm.frame().environments.snapshot_for_closure(), + env_snapshot, script_or_module, context.realm().clone(), ); diff --git a/core/engine/src/vm/flowgraph/mod.rs b/core/engine/src/vm/flowgraph/mod.rs index 2f96e6edcbd..265bfaae6f5 100644 --- a/core/engine/src/vm/flowgraph/mod.rs +++ b/core/engine/src/vm/flowgraph/mod.rs @@ -367,15 +367,15 @@ impl CodeBlock { | Instruction::CheckReturn | Instruction::BindThisValue { .. } | Instruction::CreateMappedArgumentsObject { .. } - | Instruction::CreateUnmappedArgumentsObject { .. } => { + | Instruction::CreateUnmappedArgumentsObject { .. } + | Instruction::SetArrowLexicalThis { .. } => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } Instruction::Return => { graph.add_node(previous_pc, NodeShape::Diamond, label.into(), Color::Red); } - Instruction::Reserved1 - | Instruction::Reserved2 + Instruction::Reserved2 | Instruction::Reserved3 | Instruction::Reserved4 | Instruction::Reserved5 diff --git a/core/engine/src/vm/opcode/arguments.rs b/core/engine/src/vm/opcode/arguments.rs index 47081552dc4..e1ffcb3f691 100644 --- a/core/engine/src/vm/opcode/arguments.rs +++ b/core/engine/src/vm/opcode/arguments.rs @@ -14,22 +14,21 @@ pub(crate) struct CreateMappedArgumentsObject; impl CreateMappedArgumentsObject { #[inline(always)] pub(super) fn operation(value: RegisterOperand, context: &mut Context) { - let frame = context.vm.frame(); let function_object = context .vm .stack .get_function(context.vm.frame()) .expect("there should be a function object"); - let code = frame.code_block().clone(); - let args = context.vm.stack.get_arguments(context.vm.frame()); + let code = context.vm.frame().code_block().clone(); let env = { - let frame = context.vm.frame(); + let frame = context.vm.frame_mut(); + let global = frame.realm.environment().clone(); frame .environments - .current_declarative_ref(frame.realm.environment()) + .current_declarative_gc(&global) .expect("must be declarative") - .clone() }; + let args = context.vm.stack.get_arguments(context.vm.frame()); let arguments = MappedArguments::new( &function_object, &code.mapped_arguments_binding_indices, diff --git a/core/engine/src/vm/opcode/get/function.rs b/core/engine/src/vm/opcode/get/function.rs index ce11d25f03d..66d62371632 100644 --- a/core/engine/src/vm/opcode/get/function.rs +++ b/core/engine/src/vm/opcode/get/function.rs @@ -1,5 +1,6 @@ use crate::{ Context, + builtins::function::OrdinaryFunction, vm::{ code_block::create_function_object_fast, opcode::{IndexOperand, Operation, RegisterOperand}, @@ -31,3 +32,36 @@ impl Operation for GetFunction { const INSTRUCTION: &'static str = "INST - GetFunction"; const COST: u8 = 3; } + +/// `SetArrowLexicalThis` implements the Opcode Operation for `Opcode::SetArrowLexicalThis` +/// +/// Operation: +/// - Set the captured lexical `this` on an arrow function object. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SetArrowLexicalThis; + +impl SetArrowLexicalThis { + #[inline(always)] + pub(crate) fn operation( + (function, this_value): (RegisterOperand, RegisterOperand), + context: &mut Context, + ) { + let this = context.vm.get_register(this_value.into()).clone(); + let func_obj = context + .vm + .get_register(function.into()) + .as_object() + .expect("SetArrowLexicalThis: register must hold an object") + .clone(); + func_obj + .downcast_mut::() + .expect("SetArrowLexicalThis: object must be an OrdinaryFunction") + .lexical_this = Some(this); + } +} + +impl Operation for SetArrowLexicalThis { + const NAME: &'static str = "SetArrowLexicalThis"; + const INSTRUCTION: &'static str = "INST - SetArrowLexicalThis"; + const COST: u8 = 3; +} diff --git a/core/engine/src/vm/opcode/mod.rs b/core/engine/src/vm/opcode/mod.rs index 8b61ad67b73..b4b62222b78 100644 --- a/core/engine/src/vm/opcode/mod.rs +++ b/core/engine/src/vm/opcode/mod.rs @@ -2137,8 +2137,16 @@ generate_opcodes! { /// - Output: dst CreateUnmappedArgumentsObject { dst: RegisterOperand }, - /// Reserved [`Opcode`]. - Reserved1 => Reserved, + /// Set the captured lexical `this` on an arrow function object. + /// + /// Used by the `THIS_ESCAPED_ONLY` optimisation: instead of allocating a + /// function-environment just for `FunctionSlots`, the enclosing function + /// resolves `this` and writes it directly onto each inner arrow closure. + /// + /// - Registers: + /// - function: the arrow function object (in/out) + /// - this_value: the resolved `this` value + SetArrowLexicalThis { function: RegisterOperand, this_value: RegisterOperand }, /// Reserved [`Opcode`]. Reserved2 => Reserved, /// Reserved [`Opcode`]. diff --git a/core/engine/src/vm/opcode/set/name.rs b/core/engine/src/vm/opcode/set/name.rs index be97dd7bfd3..2a6d3ba1d4a 100644 --- a/core/engine/src/vm/opcode/set/name.rs +++ b/core/engine/src/vm/opcode/set/name.rs @@ -2,7 +2,6 @@ use boa_ast::scope::{BindingLocator, BindingLocatorScope}; use crate::{ Context, JsError, JsNativeError, JsResult, - environments::Environment, vm::opcode::{IndexOperand, Operation, RegisterOperand}, }; @@ -119,17 +118,23 @@ fn verify_initialized(locator: &BindingLocator, context: &mut Context) -> JsResu "cannot assign to uninitialized binding `{}`", key.to_std_string_escaped() )), - BindingLocatorScope::Stack(index) => match context.environment_expect(index) { - Environment::Declarative(_) => Some(format!( - "cannot assign to uninitialized binding `{}`", - key.to_std_string_escaped() - )), - Environment::Object(_) if strict => Some(format!( - "cannot assign to uninitialized property `{}`", - key.to_std_string_escaped() - )), - Environment::Object(_) => None, - }, + BindingLocatorScope::Stack(index) => { + if context.vm.frame().environments.is_object_env(index) { + if strict { + Some(format!( + "cannot assign to uninitialized property `{}`", + key.to_std_string_escaped() + )) + } else { + None + } + } else { + Some(format!( + "cannot assign to uninitialized binding `{}`", + key.to_std_string_escaped() + )) + } + } }; if let Some(message) = message {