Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions benches/scripts/basic/this-arrow.js
Original file line number Diff line number Diff line change
@@ -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();
}
13 changes: 13 additions & 0 deletions core/ast/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ pub struct FunctionScopes {
pub(crate) lexical_scope: Option<Scope>,
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 {
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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,
})
}
}
21 changes: 16 additions & 5 deletions core/ast/src/scope_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 17 additions & 9 deletions core/engine/src/builtins/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions core/engine/src/builtins/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsValue>,

/// The `[[Fields]]` internal slot.
fields: ThinVec<ClassFieldDefinition>,

Expand Down Expand Up @@ -221,6 +226,7 @@ impl OrdinaryFunction {
home_object: None,
script_or_module,
realm,
lexical_this: None,
fields: ThinVec::default(),
private_methods: ThinVec::default(),
}
Expand Down Expand Up @@ -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);

Expand All @@ -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());
Expand Down
3 changes: 3 additions & 0 deletions core/engine/src/builtins/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsObject>) -> 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();
Expand Down
3 changes: 3 additions & 0 deletions core/engine/src/builtins/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 28 additions & 4 deletions core/engine/src/bytecompiler/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Scope>,
spanned_source_text: SpannedSourceText,
source_path: SourcePath,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand Down
17 changes: 17 additions & 0 deletions core/engine/src/bytecompiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions core/engine/src/environments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading
Loading